diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 23e67795..4a9c585a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,33 +1,37 @@ -**In raising this issue, I confirm the following (please check boxes, eg [X]) Failure to fill the template will close your issue:** +**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 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 codebase?:** +**How familiar are you with the the source code relevant to this issue?:** -_{replace this text with a number from 1 to 10, with 1 being not familiar, and 10 being very familiar}_ +`{Replace this with a number from 1 to 10. 1 being not familiar, and 10 being very familiar}` --- -**[BUG REPORT | OTHER]:** +**Expected behaviour:** -Please [submit your feature request here](https://discourse.pi-hole.net/c/feature-requests), so it is votable by the community. It's also easier for us to track. +`{A detailed description of what you expect to see}` -**[BUG | ISSUE] Expected Behaviour:** +**Actual behaviour:** +`{A detailed description and/or screenshots of what you do see}` -**[BUG | ISSUE] Actual Behaviour:** +**Steps to reproduce:** +`{Detailed steps of how we can reproduce this}` -**[BUG | ISSUE] Steps to reproduce:** +**Debug token provided by [uploading `pihole -d` log](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738#debug):** -- -- -- -- +`{Alphanumeric token}` -**(Optional) Debug token generated by `pihole -d`:** +**Troubleshooting undertaken, and/or other relevant information:** -`` +`{Steps of what you have done to fix this}` -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ +> * `{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 index 424bbc78..96ce4ba5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,32 @@ -**By submitting this pull request, I confirm the following (please check boxes, eg [X]) _Failure to fill the template will close your PR_:** +**By submitting this pull request, I confirm the following:** `{please fill any appropriate checkboxes, e.g: [X]}` -***Please submit all pull requests against the `development` branch. Failure to do so will delay or deny your request*** +`{Please ensure that your pull request is for the 'development' branch!}` -- [] I have read and understood the [contributors guide](https://github.com/pi-hole/pi-hole/blob/master/CONTRIBUTING.md). -- [] I have checked that [another pull request](https://github.com/pi-hole/pi-hole/pulls) for this purpose does not exist. -- [] I have considered, and confirmed that this submission will be valuable to others. -- [] I accept that this submission may not be used, and the pull request closed at the will of the maintainer. -- [] I give this submission freely, and claim no ownership to its content. - -**How familiar are you with the codebase?:** - -_{replace this text with a number from 1 to 10, with 1 being not familiar, and 10 being very familiar}_ +- [] 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)) +- [] I have Signed Off all commits. ([`git commit --signoff`](https://git-scm.com/docs/git-commit#git-commit---signoff)) --- -_{replace this line with your pull request content}_ +**What does this PR aim to accomplish?:** -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ +`{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}` + +> * `{Please delete this quoted section when opening your pull request}` +> * You must follow the template instructions. Failure to do so will result in your issue being closed. +> * Please respect that Pi-hole is developed by volunteers, who can only reply in their spare time. +> * Detail helps us understand an issue quicker, but please ensure it's relevant. diff --git a/.gitignore b/.gitignore index 91014dcd..0e0d4b99 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ *.swp __pycache__ .cache -.pullapprove.yml diff --git a/README.md b/README.md index 6f8813fa..f6d15f43 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,217 @@

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

-

- -

+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. -## The multi-platform, network-wide ad blocker +- **Easy-to-install**: our versatile installer walks you through the process, and [takes less than ten minutes](https://www.youtube.com/watch?v=vKWjx1AQYgs) +- **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 +- **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 +- **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 -Block ads for **all** your devices _without_ the need to install client-side software. The Pi-hole™ blocks ads at the DNS-level, so all your devices are protected. +----- +Codacy Grade +Travis Build Status +BountySource -- Web Browsers -- Cell Phones -- Smart TV's -- Internet-connected home automation -- Anything that communicates with the Internet +## 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` -## Your Support Still Matters - -Digital Ocean helps with our infrastructure, but our developers are all volunteers so *your donations help keep us innovating*. Sending a donation using our links below helps us offset a portion of our monthly costs. - -- ![Paypal](https://assets.pi-hole.net/static/paypal.png) [Donate via PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3J2L3Z4DHW9UY) -- ![Bitcoin](https://assets.pi-hole.net/static/Bitcoin.png) Bitcoin Address: 1GKnevUnVaQM2pQieMyeHkpr8DXfkpfAtL - -### One-Step Automated Install -1. Install a [supported operating system](https://discourse.pi-hole.net/t/hardware-software-requirements/273/1) -2. Run the command below (it downloads [this script](https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh) in case you want to read over it first!) - -### `curl -sSL https://install.pi-hole.net | bash` - -#### Alternative Semi-Automated Install Methods -_If you wish to read over the script before running it, run `nano basic-install.sh` to open the file in a text viewer._ - -##### Clone our repository and run the automated installer from your device. +## 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: +### Method 1: Clone our repository and run ``` git clone --depth 1 https://github.com/pi-hole/pi-hole.git Pi-hole -cd Pi-hole/automated\ install/ -bash basic-install.sh +cd "Pi-hole/automated install/" +sudo bash basic-install.sh ``` -##### Or - -```bash +### Method 2: Manually download the installer and run +``` wget -O basic-install.sh https://install.pi-hole.net -bash basic-install.sh +sudo bash basic-install.sh ``` -Once installed, [configure your router to have **DHCP clients use the Pi 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) and then any device that connects to your network will have ads blocked without any further configuration. Alternatively, you can manually set each device to use Pi-hole™ as their DNS server. +## Post-install: Make your network take advantage of Pi-hole -## What is Pi-hole™ and how do I install it? -

- -

+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. +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). -## Get Help Or Connect With Us On The Web +As a last resort, you can always manually set each device to use Pi-hole as their DNS server. -- [Users Forum](https://discourse.pi-hole.net/) -- [FAQs](https://discourse.pi-hole.net/c/faqs) -- [Wiki](https://github.com/pi-hole/pi-hole/wiki) -- ![Twitter](https://assets.pi-hole.net/static/twitter.png) [Tweet @The_Pi_Hole](https://twitter.com/The_Pi_Hole) -- ![Reddit](https://assets.pi-hole.net/static/reddit.png) [Reddit /r/pihole](https://www.reddit.com/r/pihole/) -- ![YouTube](https://assets.pi-hole.net/static/youtube.png) [Pi-hole channel](https://www.youtube.com/channel/UCT5kq9w0wSjogzJb81C9U0w) -- [![Join the chat at https://gitter.im/pi-hole/pi-hole](https://badges.gitter.im/pi-hole/pi-hole.svg)](https://gitter.im/pi-hole/pi-hole?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +----- -## Technical Details +## 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. -The Pi-hole™ is an **advertising-aware DNS/Web server**. If an ad domain is queried, a small Web page or GIF is delivered in place of the advertisement. +Make no mistake: **your support is absolutely vital to help keep us innovating!** -### Gravity +### Donations +Sending a donation using our links below is **extremely helpful** in offsetting a portion of our monthly expenses: -The [gravity.sh](https://github.com/pi-hole/pi-hole/blob/master/gravity.sh) does most of the magic. The script pulls in ad domains from many sources and compiles them into a single list of [over 1.6 million entries](http://jacobsalmela.com/block-millions-ads-network-wide-with-a-raspberry-pi-hole-2-0) (if you decide to use the [mahakala list](https://github.com/pi-hole/pi-hole/commit/963eacfe0537a7abddf30441c754c67ca1e40965)). This script is controlled by the `pihole` command. Please run `pihole -h` to see what commands can be run via `pihole`. + PP Donate via PayPal
BTC Bitcoin Address: 1GKnevUnVaQM2pQieMyeHkpr8DXfkpfAtL +### Alternative support +If you'd rather not donate (_which is okay!_), there are other ways you can help support us: +- [Digital Ocean](http://www.digitalocean.com/?refcode=344d234950e1) affiliate link +- [Vultr](http://www.vultr.com/?ref=7190426) affiliate link +- [UNIXstickers.com](http://unixstickers.refr.cc/jacobs) affiliate link +- [Pi-hole Swag Store](https://pi-hole.net/shop/) +- Spreading the word about our software, and how you have benefited from it -#### Other Operating Systems +### Contributing via GitHub +We welcome _everyone_ to contribute to issue reports, suggest new features, and create pull requests. -The automated install is only for a clean install of a Debian family or Fedora based system, such as the Raspberry Pi. However, this script will work for most UNIX-like systems, some with some slight **modifications** that we can help you work through. If you can install `dnsmasq` and a web server, it should work OK. If there are other platforms you'd like supported, let us know. +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. -### Web Interface +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. -The [Web interface](https://github.com/pi-hole/AdminLTE#pi-hole-admin-dashboard) will be installed automatically so you can view stats and change settings. You can find it at: +### Presentations about Pi-hole +Word-of-mouth continues to help our project grow immensely, and so we are helping make this easier for people. -`http://192.168.1.x/admin/index.php` or `http://pi.hole/admin` +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! -![Pi-hole Advanced Stats Dashboard](https://assets.pi-hole.net/static/dashboard212.png) +----- -### Whitelist and blacklist +## 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. -Domains can be whitelisted and blacklisted using either the web interface or the command line. See [the wiki page](https://github.com/pi-hole/pi-hole/wiki/Whitelisting-and-Blacklisting) for more details -

- -

+ +
+ -### Settings +----- -The settings page lets you control and configure your Pi-hole™. You can do things like: +## 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 understanding of `bash`. -- enable Pi-hole's built-in DHCP server -- exclude domains from the graphs -- configure upstream DNS servers -- and more! +Pi-hole Blacklist Demo -![Settings page](https://assets.pi-hole.net/static/settings212.png) +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! -#### Built-in DHCP Server +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. -Pi-hole™ ships with a built-in DHCP server. This allows you to let your network devices use Pi-hole™ as their DNS server if your router does not let you adjust the DHCP options. -

- -

+### 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! -## API +Pi-hole Dashboard -A basic read-only API can be accessed at `/admin/api.php`. It returns the following JSON: +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! -``` json -{ - "domains_being_blocked": "136708", - "dns_queries_today": "18108", - "ads_blocked_today": "14648", - "ads_percentage_today": "80.89" -} -``` +There are several ways to [access the dashboard](https://discourse.pi-hole.net/t/how-do-i-access-pi-holes-dashboard-admin-interface/3168): -The same output can be achieved on the CLI by running `chronometer.sh -j` +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) -## Real-time Statistics +## The Faster-Than-Light Engine +The [FTL Engine](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, FTL does this all *very quickly*! -You can view [real-time stats](https://discourse.pi-hole.net/t/how-do-i-view-my-pi-holes-stats-over-ssh-or-on-an-lcd-using-chronometer/240) via `ssh` or on an [2.8" LCD screen](http://amzn.to/1P0q1Fj). This is accomplished via [`chronometer.sh`](https://github.com/pi-hole/pi-hole/blob/master/advanced/Scripts/chronometer.sh). ![Pi-hole LCD](http://i.imgur.com/nBEqycp.jpg) +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 -## Pi-hole™ Projects +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). -- [An ad blocking Magic Mirror](https://zonksec.com/blog/magic-mirror-dns-filtering/#dnssoftware) -- [Pi-hole stats in your Mac's menu bar](https://getbitbar.com/plugins/Network/pi-hole.1m.py) -- [Get LED alerts for each blocked ad](http://thetimmy.silvernight.org/pages/endisbutton/) -- [Pi-hole on Ubuntu 14.04 on VirtualBox](http://hbalagtas.blogspot.com/2016/02/adblocking-with-pi-hole-and-ubuntu-1404.html) -- [Docker Pi-hole container (x86 and ARM)](https://hub.docker.com/r/diginc/pi-hole/) -- [Splunk: Pi-hole Visualiser](https://splunkbase.splunk.com/app/3023/) -- [Pi-hole Chrome extension](https://chrome.google.com/webstore/detail/pi-hole-list-editor/hlnoeoejkllgkjbnnnhfolapllcnaglh) ([open source](https://github.com/packtloss/pihole-extension)) -- [Go Bananas for CHiP-hole ad blocking](https://www.hackster.io/jacobsalmela/chip-hole-network-wide-ad-blocker-98e037) -- [Sky-Hole](http://dlaa.me/blog/post/skyhole) -- [Pi-hole in the Cloud!](http://blog.codybunch.com/2015/07/28/Pi-Hole-in-the-cloud/) -- [unRaid-hole](https://github.com/spants/unraidtemplates/blob/master/Spants/unRaid-hole.xml#L13)--[Repo and more info](http://lime-technology.com/forum/index.php?PHPSESSID=c0eae3e5ef7e521f7866034a3336489d&topic=38486.0) -- [Pi-hole on/off button](http://thetimmy.silvernight.org/pages/endisbutton/) -- [Minibian Pi-hole](http://munkjensen.net/wiki/index.php/See_my_Pi-Hole#Minibian_Pi-hole) -- [Windows Tray Stat Application](https://github.com/goldbattle/copernicus) -- [Let your blink1 device blink when Pi-hole filters ads](https://gist.github.com/elpatron68/ec0b4c582e5abf604885ac1e068d233f) -- [Pi-hole Prometheus exporter](https://github.com/nlamirault/pihole_exporter): a [Prometheus](https://prometheus.io/) exporter for Pi-hole -- [Pi-hole Droid - open source Android client](https://github.com/friimaind/pi-hole-droid) -- [Windows DNS Swapper](https://github.com/roots84/DNS-Swapper), see [#1400](https://github.com/pi-hole/pi-hole/issues/1400) +----- + +## The Origin Of Pi-hole +Pi-hole being a **advertising-aware DNS/Web server**, makes use of the following technologies: + +* [`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) - webserver 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 + +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 setup 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. + +----- + +## Pi-hole Projects +- [The Big Blocklist Collection](https://wally3k.github.io) +- [Docker Pi-hole container (x86 and ARM)](https://hub.docker.com/r/diginc/pi-hole/) +- [Pi-Hole in the cloud](http://blog.codybunch.com/2015/07/28/Pi-Hole-in-the-cloud/) +- [Pie in the Sky-Hole [A Pi-Hole in the cloud for ad-blocking via DNS]](https://dlaa.me/blog/post/skyhole) +- [Pi-hole Enable/Disable Button](http://thetimmy.silvernight.org/pages/endisbutton/) +- [Minibian Pi-hole](https://munkjensen.net/wiki/index.php/See_my_Pi-Hole#Minibian_Pi-hole) +- [CHiP-hole: Network-wide Ad-blocker](https://www.hackster.io/jacobsalmela/chip-hole-network-wide-ad-blocker-98e037) +- [Chrome Extension: Pi-Hole List Editor](https://chrome.google.com/webstore/detail/pi-hole-list-editor/hlnoeoejkllgkjbnnnhfolapllcnaglh) ([Source Code](https://github.com/packtloss/pihole-extension)) +- [Splunk: Pi-hole Visualiser](https://splunkbase.splunk.com/app/3023/) +- [Adblocking with P-hole and Ubuntu 14.04 on VirtualBox](https://hbalagtas.blogspot.com.au/2016/02/adblocking-with-pi-hole-and-ubuntu-1404.html) +- [Pi-hole stats in your Mac's menu bar](https://getbitbar.com/plugins/Network/pi-hole.1m.py) +- [Pi-hole unRAID Template](https://forums.lime-technology.com/topic/36810-support-spants-nodered-mqtt-dashing-couchdb/) +- [Copernicus: Windows Tray Application](https://github.com/goldbattle/copernicus) +- [Let your blink1 device blink when Pi-hole filters ads](https://gist.github.com/elpatron68/ec0b4c582e5abf604885ac1e068d233f) +- [Pi-hole metrics](https://github.com/nlamirault/pihole_exporter) exporter for [Prometheus](https://prometheus.io/) +- [Magic Mirror with DNS Filtering](https://zonksec.com/blog/magic-mirror-dns-filtering/#dnssoftware) +- [Pi-hole Droid: Android client](https://github.com/friimaind/pi-hole-droid) + +----- ## Coverage - -- [Adafruit livestream install](https://www.youtube.com/watch?v=eg4u2j1HYlI) -- [TekThing: 5 fun, easy projects for a Raspberry Pi](https://youtu.be/QwrKlyC2kdM?t=1m42s) -- [Pi-hole on Adafruit's blog](https://blog.adafruit.com/2016/03/04/pi-hole-is-a-black-hole-for-internet-ads-piday-raspberrypi-raspberry_pi/) -- [The Defrag Show - MSDN/Channel 9](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) -- [MacObserver Podcast 585](http://www.macobserver.com/tmo/podcast/macgeekgab-585) -- [Medium: Block All Ads For $53](https://medium.com/@robleathern/block-ads-on-all-home-devices-for-53-18-a5f1ec139693#.gj1xpgr5d) -- [MakeUseOf: Adblock Everywhere, The Pi-hole Way](http://www.makeuseof.com/tag/adblock-everywhere-raspberry-pi-hole-way/) -- [Lifehacker: Turn Your Pi Into An Ad Blocker With A Single Command](http://lifehacker.com/turn-a-raspberry-pi-into-an-ad-blocker-with-a-single-co-1686093533)! -- [Pi-hole on TekThing](https://youtu.be/8Co59HU2gY0?t=2m) -- [Pi-hole on Security Now! Podcast](http://www.youtube.com/watch?v=p7-osq_y8i8&t=100m26s) -- [Foolish Tech Show](https://youtu.be/bYyena0I9yc?t=2m4s) -- [Pi-hole on Ubuntu](http://www.boyter.org/2015/12/pi-hole-ubuntu-14-04/) -- [Catchpoint: iOS 9 Ad Blocking](http://blog.catchpoint.com/2015/09/14/ad-blocking-apple/) -- [Build an Ad-Blocker for less than 10$ with Orange-Pi](http://www.devacron.com/orangepi-zero-as-an-ad-block-server-with-pi-hole/) +- [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/) +- [MakeUseOf: Adblock Everywhere: The Raspberry Pi-Hole Way](http://www.makeuseof.com/tag/adblock-everywhere-raspberry-pi-hole-way/) +- [Catchpoint: Ad-Blocking on Apple iOS9: Valuing the End User Experience](http://blog.catchpoint.com/2015/09/14/ad-blocking-apple/) +- [Security Now Netcast: Pi-hole](https://www.youtube.com/watch?v=p7-osq_y8i8&t=100m26s) +- [TekThing: Raspberry Pi-Hole Makes Ads Disappear!](https://youtu.be/8Co59HU2gY0?t=2m) +- [Foolish Tech Show](https://youtu.be/bYyena0I9yc?t=2m4s) +- [Block Ads on All Home Devices for $53.18](https://medium.com/@robleathern/block-ads-on-all-home-devices-for-53-18-a5f1ec139693#.gj1xpgr5d) +- [Pi-Hole for Ubuntu 14.04](http://www.boyter.org/2015/12/pi-hole-ubuntu-14-04/) +- [MacObserver Podcast 585](https://www.macobserver.com/tmo/podcast/macgeekgab-585) +- [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) +- [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/) +- [Digital Trends: 5 Fun, Easy Projects You Can Try With a $35 Raspberry Pi](https://youtu.be/QwrKlyC2kdM?t=1m42s) +- [Adafruit: Raspberry Pi Quick Look at Pi Hole ad blocking server with Tony D](https://www.youtube.com/watch?v=eg4u2j1HYlI) +- [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/) +- [Linux Pro: The Hole Truth](http://www.linuxpromagazine.com/Issues/2017/200/The-sysadmin-s-daily-grind-Pi-hole) +- [CryptoAUSTRALIA: How We Tried 5 Privacy Focused Raspberry Pi Projects](https://blog.cryptoaustralia.org.au/2017/10/05/5-privacy-focused-raspberry-pi-projects/) +- [CryptoAUSTRALIA: Pi-hole Workshop](https://blog.cryptoaustralia.org.au/2017/11/02/pi-hole-network-wide-ad-blocker/) +- [Know How 355: Killing ads with a Raspberry Pi-Hole!](https://www.twit.tv/shows/know-how/episodes/355) diff --git a/advanced/01-pihole.conf b/advanced/01-pihole.conf index 79735c15..f7b78ab0 100644 --- a/advanced/01-pihole.conf +++ b/advanced/01-pihole.conf @@ -21,8 +21,8 @@ ############################################################################### addn-hosts=/etc/pihole/gravity.list -addn-hosts=/etc/pihole/local.list addn-hosts=/etc/pihole/black.list +addn-hosts=/etc/pihole/local.list domain-needed @@ -42,6 +42,6 @@ cache-size=10000 log-queries log-facility=/var/log/pihole.log -local-ttl=300 +local-ttl=2 log-async diff --git a/advanced/Scripts/COL_TABLE b/advanced/Scripts/COL_TABLE new file mode 100644 index 00000000..57aab4dd --- /dev/null +++ b/advanced/Scripts/COL_TABLE @@ -0,0 +1,49 @@ +# Determine if terminal is capable of showing colours +if [[ -t 1 ]] && [[ $(tput colors) -ge 8 ]]; then + # Bold and underline may not show up on all clients + # If something MUST be emphasised, use both + COL_BOLD='' + COL_ULINE='' + + COL_NC='' + COL_GRAY='' + COL_RED='' + COL_GREEN='' + COL_YELLOW='' + COL_BLUE='' + COL_PURPLE='' + COL_CYAN='' +else + # Provide empty variables for `set -u` + COL_BOLD="" + COL_ULINE="" + + COL_NC="" + COL_GRAY="" + COL_RED="" + COL_GREEN="" + COL_YELLOW="" + COL_BLUE="" + COL_PURPLE="" + COL_CYAN="" +fi + +# Deprecated variables +COL_WHITE="${COL_BOLD}" +COL_BLACK="${COL_NC}" +COL_LIGHT_BLUE="${COL_BLUE}" +COL_LIGHT_GREEN="${COL_GREEN}" +COL_LIGHT_CYAN="${COL_CYAN}" +COL_LIGHT_RED="${COL_RED}" +COL_URG_RED="${COL_RED}${COL_BOLD}${COL_ULINE}" +COL_LIGHT_PURPLE="${COL_PURPLE}" +COL_BROWN="${COL_YELLOW}" +COL_LIGHT_GRAY="${COL_GRAY}" +COL_DARK_GRAY="${COL_GRAY}" + +TICK="[${COL_GREEN}✓${COL_NC}]" +CROSS="[${COL_RED}✗${COL_NC}]" +INFO="[i]" +QST="[?]" +DONE="${COL_GREEN} done!${COL_NC}" +OVER="\\r" diff --git a/advanced/Scripts/chronometer.sh b/advanced/Scripts/chronometer.sh index d9b7d05b..8599e995 100755 --- a/advanced/Scripts/chronometer.sh +++ b/advanced/Scripts/chronometer.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=SC1090,SC1091 # 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. @@ -7,6 +8,7 @@ # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. +LC_NUMERIC=C # Retrieve stats from FTL engine pihole-FTL() { @@ -32,43 +34,74 @@ pihole-FTL() { exec 3<&- fi else - echo -e "${COL_LIGHT_RED}FTL offline${COL_NC}" + echo "0" fi } -# Print spaces to align right-side content +# Print spaces to align right-side additional text printFunc() { - txt_len="${#2}" - - # Reduce string length when using colour code - [ "${2:0:1}" == "" ] && txt_len=$((txt_len-7)) - - if [[ "$3" == "last" ]]; then - # Prevent final line from printing trailing newline - scr_size=( $(stty size 2>/dev/null || echo 24 80) ) - scr_width="${scr_size[1]}" - - title_len="${#1}" - spc_num=$(( (scr_width - title_len) - txt_len )) - [[ "$spc_num" -lt 0 ]] && spc_num="0" - spc=$(printf "%${spc_num}s") - - printf "%s%s$spc" "$1" "$2" - else - # Determine number of spaces for padding - spc_num=$(( 20 - txt_len )) - [[ "$spc_num" -lt 0 ]] && spc_num="0" - spc=$(printf "%${spc_num}s") + local text_last - # Print string (Max 20 characters, prevents overflow) - printf "%s%s$spc" "$1" "${2:0:20}" + title="$1" + title_len="${#title}" + + text_main="$2" + text_main_nocol="$text_main" + if [[ "${text_main:0:1}" == "" ]]; then + text_main_nocol=$(sed 's/\[[0-9;]\{1,5\}m//g' <<< "$text_main") + fi + text_main_len="${#text_main_nocol}" + + text_addn="$3" + if [[ "$text_addn" == "last" ]]; then + text_addn="" + text_last="true" + fi + + # If there is additional text, define max length of text_main + if [[ -n "$text_addn" ]]; then + case "$scr_cols" in + [0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-4]) text_main_max_len="9";; + 4[5-9]) text_main_max_len="14";; + *) text_main_max_len="19";; + esac + fi + + [[ -z "$text_addn" ]] && text_main_max_len="$(( scr_cols - title_len ))" + + # Remove excess characters from main text + if [[ "$text_main_len" -gt "$text_main_max_len" ]]; then + # Trim text without colours + 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}" + fi + + # Determine amount of spaces for each line + if [[ -n "$text_last" ]]; then + # Move cursor to end of screen + spc_num=$(( scr_cols - ( title_len + text_main_len ) )) + else + spc_num=$(( text_main_max_len - text_main_len )) + fi + + [[ "$spc_num" -le 0 ]] && spc_num="0" + spc=$(printf "%${spc_num}s") + #spc="${spc// /.}" # Debug: Visualise spaces + + printf "%s%s$spc" "$title" "$text_main" + + if [[ -n "$text_addn" ]]; then + printf "%s(%s)%s\\n" "$COL_NC$COL_DARK_GRAY" "$text_addn" "$COL_NC" + else + # Do not print trailing newline on final line + [[ -z "$text_last" ]] && printf "%s\\n" "$COL_NC" fi } # Perform on first Chrono run (not for JSON formatted string) get_init_stats() { - LC_NUMERIC=C - calcFunc(){ awk "BEGIN {print $*}"; } + calcFunc(){ awk "BEGIN {print $*}" 2> /dev/null; } # Convert bytes to human-readable format hrBytes() { @@ -90,33 +123,53 @@ get_init_stats() { # Convert seconds to human-readable format hrSecs() { - day=$(( $1/60/60/24 )); hrs=$(( $1/3600%24 )); mins=$(( ($1%3600)/60 )); secs=$(( $1%60 )) + day=$(( $1/60/60/24 )); hrs=$(( $1/3600%24 )) + mins=$(( ($1%3600)/60 )); secs=$(( $1%60 )) [[ "$day" -ge "2" ]] && plu="s" [[ "$day" -ge "1" ]] && days="$day day${plu}, " || days="" - printf "%s%02d:%02d:%02d\n" "$days" "$hrs" "$mins" "$secs" - } + printf "%s%02d:%02d:%02d\\n" "$days" "$hrs" "$mins" "$secs" + } # Set Colour Codes coltable="/opt/pihole/COL_TABLE" if [[ -f "${coltable}" ]]; then source ${coltable} else - COL_NC='' - COL_DARK_GRAY='' - COL_LIGHT_GREEN='' - COL_LIGHT_BLUE='' - COL_LIGHT_RED='' - COL_YELLOW='' - COL_LIGHT_RED='' - COL_URG_RED='' + COL_NC="" + COL_DARK_GRAY="" + COL_LIGHT_GREEN="" + COL_LIGHT_BLUE="" + COL_LIGHT_RED="" + COL_YELLOW="" + COL_LIGHT_RED="" + COL_URG_RED="" fi - # Get RPi model number, or OS distro info + # Get RPi throttle state (RPi 3B only) & model number, or OS distro info if command -v vcgencmd &> /dev/null; then - sys_rev=$(awk '/Revision/ {print $3}' < /proc/cpuinfo) - case "$sys_rev" in + local sys_throttle_raw + local sys_rev_raw + + sys_throttle_raw=$(vgt=$(sudo vcgencmd get_throttled); echo "${vgt##*x}") + + # Active Throttle Notice: http://bit.ly/2gnunOo + if [[ "$sys_throttle_raw" != "0" ]]; then + case "$sys_throttle_raw" in + *0001) thr_type="${COL_YELLOW}Under Voltage";; + *0002) thr_type="${COL_LIGHT_BLUE}Arm Freq Cap";; + *0003) thr_type="${COL_YELLOW}UV${COL_DARK_GRAY},${COL_NC} ${COL_LIGHT_BLUE}AFC";; + *0004) thr_type="${COL_LIGHT_RED}Throttled";; + *0005) thr_type="${COL_YELLOW}UV${COL_DARK_GRAY},${COL_NC} ${COL_LIGHT_RED}TT";; + *0006) thr_type="${COL_LIGHT_BLUE}AFC${COL_DARK_GRAY},${COL_NC} ${COL_LIGHT_RED}TT";; + *0007) thr_type="${COL_YELLOW}UV${COL_DARK_GRAY},${COL_NC} ${COL_LIGHT_BLUE}AFC${COL_DARK_GRAY},${COL_NC} ${COL_LIGHT_RED}TT";; + esac + [[ -n "$thr_type" ]] && sys_throttle="$thr_type${COL_DARK_GRAY}" + fi + + sys_rev_raw=$(awk '/Revision/ {print $3}' < /proc/cpuinfo) + case "$sys_rev_raw" in 000[2-6]) sys_model=" 1, Model B";; # 256MB - 000[7-9]) sys_model=" 1, Model A" ;; # 256MB + 000[7-9]) sys_model=" 1, Model A";; # 256MB 000d|000e|000f) sys_model=" 1, Model B";; # 512MB 0010|0013) sys_model=" 1, Model B+";; # 512MB 0012|0015) sys_model=" 1, Model A+";; # 256MB @@ -126,7 +179,7 @@ get_init_stats() { 90009[2-3]|920093) sys_model=" Zero";; # 512MB 9000c1) sys_model=" Zero W";; # 512MB a02082|a[2-3]2082) sys_model=" 3, Model B";; # 1GB - *) sys_model="" ;; + *) sys_model="";; esac sys_type="Raspberry Pi$sys_model" else @@ -137,7 +190,6 @@ get_init_stats() { # Get core count sys_cores=$(grep -c "^processor" /proc/cpuinfo) - [[ "$sys_cores" -ne 1 ]] && sys_cores_plu="cores" || sys_cores_plu="core" # Test existence of clock speed file for ARM CPU if [[ -f "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" ]]; then @@ -168,80 +220,97 @@ get_sys_stats() { # Update every 12 refreshes (Def: every 60s) count=$((count+1)) if [[ "$count" == "1" ]] || (( "$count" % 12 == 0 )); then + # Do not source setupVars if file does not exist [[ -n "$setupVars" ]] && source "$setupVars" - - - ph_ver_raw=($(pihole -v -c 2> /dev/null | sed -n 's/^.* v/v/p')) + + 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]}" else - ph_core_ver="${COL_LIGHT_RED}API unavailable${COL_NC}" + ph_core_ver="-1" fi - + sys_name=$(hostname) - + [[ -n "$TEMPERATUREUNIT" ]] && temp_unit="$TEMPERATUREUNIT" || temp_unit="c" - + # Get storage stats for partition mounted on / - disk_raw=($(df -B1 / 2> /dev/null | awk 'END{ print $3,$2,$5 }')) + read -r -a disk_raw <<< "$(df -B1 / 2> /dev/null | awk 'END{ print $3,$2,$5 }')" disk_used="${disk_raw[0]}" disk_total="${disk_raw[1]}" disk_perc="${disk_raw[2]}" - + net_gateway=$(route -n | awk '$4 == "UG" {print $2;exit}') - + # Get DHCP stats, if feature is enabled if [[ "$DHCP_ACTIVE" == "true" ]]; then - ph_dhcp_eip="${DHCP_END##*.}" ph_dhcp_max=$(( ${DHCP_END##*.} - ${DHCP_START##*.} + 1 )) fi - - # Get alt DNS server, or print total count of alt DNS servers - if [[ -z "${PIHOLE_DNS_3}" ]]; then - ph_alts="${PIHOLE_DNS_2}" - else - dns_count="0" - [[ -n "${PIHOLE_DNS_2}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_3}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_4}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_5}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_6}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_7}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_8}" ]] && dns_count=$((dns_count+1)) - [[ -n "${PIHOLE_DNS_9}" ]] && dns_count="$dns_count+" - ph_alts="${dns_count} others" - fi + + # Get DNS server count + dns_count="0" + [[ -n "${PIHOLE_DNS_1}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_2}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_3}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_4}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_5}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_6}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_7}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_8}" ]] && dns_count=$((dns_count+1)) + [[ -n "${PIHOLE_DNS_9}" ]] && dns_count="$dns_count+" fi - + + # Get screen size + read -r -a scr_size <<< "$(stty size 2>/dev/null || echo 24 80)" + scr_lines="${scr_size[0]}" + scr_cols="${scr_size[1]}" + + # Determine Chronometer size behaviour + if [[ "$scr_cols" -ge 58 ]]; then + chrono_width="large" + elif [[ "$scr_cols" -gt 40 ]]; then + chrono_width="medium" + else + chrono_width="small" + fi + + # Determine max length of divider string + scr_line_len=$(( scr_cols - 2 )) + [[ "$scr_line_len" -ge 58 ]] && scr_line_len="58" + scr_line_str=$(printf "%${scr_line_len}s") + scr_line_str="${scr_line_str// /—}" + sys_uptime=$(hrSecs "$(cut -d. -f1 /proc/uptime)") sys_loadavg=$(cut -d " " -f1,2,3 /proc/loadavg) - - # Get CPU usage, only counting processes over 1% CPU as active + + # Get CPU usage, only counting processes over 1% as active + # shellcheck disable=SC2009 cpu_raw=$(ps -eo pcpu,rss --no-headers | grep -E -v " 0") cpu_tasks=$(wc -l <<< "$cpu_raw") cpu_taskact=$(sed -r "/(^ 0.)/d" <<< "$cpu_raw" | wc -l) cpu_perc=$(awk '{sum+=$1} END {printf "%.0f\n", sum/'"$sys_cores"'}' <<< "$cpu_raw") - + # Get CPU clock speed if [[ -n "$scaling_freq_file" ]]; then cpu_mhz=$(( $(< /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq) / 1000 )) else - cpu_mhz=$(lscpu | awk -F "[ .]+" '/MHz/ {print $4;exit}') + cpu_mhz=$(lscpu | awk -F ":" '/MHz/ {print $2;exit}') + cpu_mhz=$(printf "%.0f" "${cpu_mhz//[[:space:]]/}") fi - - # Determine correct string format for CPU clock speed + + # Determine whether to display CPU clock speed as MHz or GHz if [[ -n "$cpu_mhz" ]]; then - [[ "$cpu_mhz" -le "999" ]] && cpu_freq="$cpu_mhz MHz" || cpu_freq="$(calcFunc "$cpu_mhz"/1000) Ghz" - [[ -n "$cpu_freq" ]] && cpu_freq_str=" @ $cpu_freq" || cpu_freq_str="" + [[ "$cpu_mhz" -le "999" ]] && cpu_freq="$cpu_mhz MHz" || cpu_freq="$(printf "%.1f" $(calcFunc "$cpu_mhz"/1000)) GHz" + [[ "${cpu_freq}" == *".0"* ]] && cpu_freq="${cpu_freq/.0/}" fi - + # Determine colour for temperature if [[ -n "$temp_file" ]]; then if [[ "$temp_unit" == "C" ]]; then - cpu_temp=$(printf "%'.0fc\n" "$(calcFunc "$(< $temp_file) / 1000")") - + cpu_temp=$(printf "%.0fc\\n" "$(calcFunc "$(< $temp_file) / 1000")") + case "${cpu_temp::-1}" in -*|[0-9]|[1-3][0-9]) cpu_col="$COL_LIGHT_BLUE";; 4[0-9]) cpu_col="";; @@ -249,13 +318,13 @@ get_sys_stats() { 6[0-9]) cpu_col="$COL_LIGHT_RED";; *) 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" - + 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")") - + cpu_temp=$(printf "%.0ff\\n" "$(calcFunc "($(< $temp_file) / 1000) * 9 / 5 + 32")") + case "${cpu_temp::-1}" in -*|[0-9]|[0-9][0-9]) cpu_col="$COL_LIGHT_BLUE";; 1[0-1][0-9]) cpu_col="";; @@ -263,132 +332,201 @@ get_sys_stats() { 1[4-5][0-9]) cpu_col="$COL_LIGHT_RED";; *) cpu_col="$COL_URG_RED";; esac - - cpu_temp_str=", $cpu_col$cpu_temp$COL_NC$COL_DARK_GRAY" - + + cpu_temp_str=" @ $cpu_col$cpu_temp$COL_NC$COL_DARK_GRAY" + else - cpu_temp_str=$(printf ", %'.0fk\n" "$(calcFunc "($(< $temp_file) / 1000) + 273.15")") + cpu_temp_str=$(printf " @ %.0fk\\n" "$(calcFunc "($(< $temp_file) / 1000) + 273.15")") fi else cpu_temp_str="" fi - - ram_raw=($(awk '/MemTotal:/{total=$2} /MemFree:/{free=$2} /Buffers:/{buffers=$2} /^Cached:/{cached=$2} END {printf "%.0f %.0f %.0f", (total-free-buffers-cached)*100/total, (total-free-buffers-cached)*1024, total*1024}' /proc/meminfo)) + + read -r -a ram_raw <<< "$(awk '/MemTotal:/{total=$2} /MemFree:/{free=$2} /Buffers:/{buffers=$2} /^Cached:/{cached=$2} END {printf "%.0f %.0f %.0f", (total-free-buffers-cached)*100/total, (total-free-buffers-cached)*1024, total*1024}' /proc/meminfo)" ram_perc="${ram_raw[0]}" ram_used="${ram_raw[1]}" ram_total="${ram_raw[2]}" - + if [[ "$(pihole status web 2> /dev/null)" == "1" ]]; then ph_status="${COL_LIGHT_GREEN}Active" else - ph_status="${COL_LIGHT_RED}Inactive" + ph_status="${COL_LIGHT_RED}Offline" fi - + if [[ "$DHCP_ACTIVE" == "true" ]]; then - ph_dhcp_num=$(wc -l 2> /dev/null < "/etc/pihole/dhcp.leases") + local ph_dhcp_range + + ph_dhcp_range=$(seq -s "|" -f "${DHCP_START%.*}.%g" "${DHCP_START##*.}" "${DHCP_END##*.}") + + # Count dynamic leases from available range, and not static leases + ph_dhcp_num=$(grep -cE "$ph_dhcp_range" "/etc/pihole/dhcp.leases") + ph_dhcp_percent=$(( ph_dhcp_num * 100 / ph_dhcp_max )) fi } get_ftl_stats() { local stats_raw - - stats_raw=($(pihole-FTL "stats")) - domains_being_blocked_raw="${stats_raw[1]}" - dns_queries_today_raw="${stats_raw[3]}" - ads_blocked_today_raw="${stats_raw[5]}" - ads_percentage_today_raw="${stats_raw[7]}" + + mapfile -t stats_raw < <(pihole-FTL "stats") + domains_being_blocked_raw="${stats_raw[0]#* }" + dns_queries_today_raw="${stats_raw[1]#* }" + ads_blocked_today_raw="${stats_raw[2]#* }" + ads_percentage_today_raw="${stats_raw[3]#* }" + queries_forwarded_raw="${stats_raw[5]#* }" + queries_cached_raw="${stats_raw[6]#* }" # Only retrieve these stats when not called from jsonFunc if [[ -z "$1" ]]; then - local recent_blocked_raw local top_ad_raw local top_domain_raw local top_client_raw - - domains_being_blocked=$(printf "%'.0f\n" "${domains_being_blocked_raw}") - dns_queries_today=$(printf "%'.0f\n" "${dns_queries_today_raw}") - ads_blocked_today=$(printf "%'.0f\n" "${ads_blocked_today_raw}") - ads_percentage_today=$(printf "%'.0f\n" "${ads_percentage_today_raw}") - - recent_blocked_raw=$(pihole-FTL recentBlocked) - top_ad_raw=($(pihole-FTL "top-ads (1)")) - top_domain_raw=($(pihole-FTL "top-domains (1)")) - top_client_raw=($(pihole-FTL "top-clients (1)")) - - # Limit strings to 40 characters to prevent overflow - recent_blocked="${recent_blocked_raw:0:40}" - top_ad="${top_ad_raw[2]:0:40}" - top_domain="${top_domain_raw[2]:0:40}" - [[ "${top_client_raw[3]}" ]] && top_client="${top_client_raw[3]:0:40}" || top_client="${top_client_raw[2]:0:40}" + + domains_being_blocked=$(printf "%.0f\\n" "${domains_being_blocked_raw}" 2> /dev/null) + dns_queries_today=$(printf "%.0f\\n" "${dns_queries_today_raw}") + ads_blocked_today=$(printf "%.0f\\n" "${ads_blocked_today_raw}") + ads_percentage_today=$(printf "%'.0f\\n" "${ads_percentage_today_raw}") + queries_cached_percentage=$(printf "%.0f\\n" "$(calcFunc "$queries_cached_raw * 100 / ( $queries_forwarded_raw + $queries_cached_raw )")") + recent_blocked=$(pihole-FTL recentBlocked) + read -r -a top_ad_raw <<< "$(pihole-FTL "top-ads (1)")" + read -r -a top_domain_raw <<< "$(pihole-FTL "top-domains (1)")" + read -r -a top_client_raw <<< "$(pihole-FTL "top-clients (1)")" + + top_ad="${top_ad_raw[2]}" + top_domain="${top_domain_raw[2]}" + if [[ "${top_client_raw[3]}" ]]; then + top_client="${top_client_raw[3]}" + else + top_client="${top_client_raw[2]}" + fi fi } +get_strings() { + # Expand or contract strings depending on screen size + if [[ "$chrono_width" == "large" ]]; then + phc_str=" ${COL_DARK_GRAY}Core" + lte_str=" ${COL_DARK_GRAY}Web" + ftl_str=" ${COL_DARK_GRAY}FTL" + api_str="${COL_LIGHT_RED}API Offline" + + host_info="$sys_type" + sys_info="$sys_throttle" + sys_info2="Active: $cpu_taskact of $cpu_tasks tasks" + used_str="Used: " + leased_str="Leased: " + domains_being_blocked=$(printf "%'.0f" "$domains_being_blocked") + ads_blocked_today=$(printf "%'.0f" "$ads_blocked_today") + dns_queries_today=$(printf "%'.0f" "$dns_queries_today") + ph_info="Blocking: $domains_being_blocked sites" + total_str="Total: " + else + phc_str=" ${COL_DARK_GRAY}Core" + lte_str=" ${COL_DARK_GRAY}Web" + ftl_str=" ${COL_DARK_GRAY}FTL" + api_str="${COL_LIGHT_RED}API Down" + ph_info="$domains_being_blocked blocked" + fi + + [[ "$sys_cores" -ne 1 ]] && sys_cores_txt="${sys_cores}x " + cpu_info="$sys_cores_txt$cpu_freq$cpu_temp_str" + ram_info="$used_str$(hrBytes "$ram_used") of $(hrBytes "$ram_total")" + disk_info="$used_str$(hrBytes "$disk_used") of $(hrBytes "$disk_total")" + + 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" + dns_info="$dns_count DNS servers" + + [[ "$recent_blocked" == "0" ]] && recent_blocked="${COL_LIGHT_RED}FTL offline${COL_NC}" +} + chronoFunc() { get_init_stats - + for (( ; ; )); do get_sys_stats get_ftl_stats - - # Do not print LTE/FTL strings if API is unavailable - ph_core_str=" ${COL_DARK_GRAY}Pi-hole: $ph_core_ver${COL_NC}" - if [[ -n "$ph_lte_ver" ]]; then - ph_lte_str=" ${COL_DARK_GRAY}AdminLTE: $ph_lte_ver${COL_NC}" - ph_ftl_str=" ${COL_DARK_GRAY}FTL: $ph_ftl_ver${COL_NC}" - fi - - clear - - echo -e "|¯¯¯(¯)__|¯|_ ___|¯|___$ph_core_str -| ¯_/¯|__| ' \/ _ \ / -_)$ph_lte_str -|_| |_| |_||_\___/_\___|$ph_ftl_str - ${COL_DARK_GRAY}——————————————————————————————————————————————————————————${COL_NC}" + get_strings - printFunc " Hostname: " "$sys_name" - [ -n "$sys_type" ] && printf "%s(%s)%s\n" "$COL_DARK_GRAY" "$sys_type" "$COL_NC" || printf "\n" - - printf "%s\n" " Uptime: $sys_uptime" - - printFunc " Task Load: " "$sys_loadavg" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Active: $cpu_taskact of $cpu_tasks tasks" "$COL_NC" - - printFunc " CPU usage: " "$cpu_perc%" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "$sys_cores $sys_cores_plu$cpu_freq_str$cpu_temp_str" "$COL_NC" - - printFunc " RAM usage: " "$ram_perc%" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Used: $(hrBytes "$ram_used") of $(hrBytes "$ram_total")" "$COL_NC" - - printFunc " HDD usage: " "$disk_perc" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Used: $(hrBytes "$disk_used") of $(hrBytes "$disk_total")" "$COL_NC" - - printFunc " LAN addr: " "${IPV4_ADDRESS/\/*/}" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Gateway: $net_gateway" "$COL_NC" - - if [[ "$DHCP_ACTIVE" == "true" ]]; then - printFunc " DHCP: " "$DHCP_START to $ph_dhcp_eip" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Leased: $ph_dhcp_num of $ph_dhcp_max" "$COL_NC" + # Strip excess development version numbers + if [[ "$ph_core_ver" != "-1" ]]; then + phc_ver_str="$phc_str: ${ph_core_ver%-*}${COL_NC}" + lte_ver_str="$lte_str: ${ph_lte_ver%-*}${COL_NC}" + ftl_ver_str="$ftl_str: ${ph_ftl_ver%-*}${COL_NC}" + else + phc_ver_str="$phc_str: $api_str${COL_NC}" fi - - printFunc " Pi-hole: " "$ph_status" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Blocking: $domains_being_blocked sites" "$COL_NC" - - printFunc " Ads Today: " "$ads_percentage_today%" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "$ads_blocked_today of $dns_queries_today queries" "$COL_NC" - - printFunc " Fwd DNS: " "$PIHOLE_DNS_1" - printf "%s(%s)%s\n" "$COL_DARK_GRAY" "Alt DNS: $ph_alts" "$COL_NC" - - echo -e " ${COL_DARK_GRAY}——————————————————————————————————————————————————————————${COL_NC}" - echo " Recently blocked: $recent_blocked" - echo " Top Advertiser: $top_ad" - echo " Top Domain: $top_domain" - printFunc " Top Client: " "$top_client" "last" - - if [[ "$1" == "exit" ]]; then + + # Get refresh number + if [[ "$*" == *"-r"* ]]; then + num="$*" + num="${num/*-r /}" + num="${num/ */}" + num_str="Refresh set for every $num seconds" + else + num_str="" + fi + + clear + + # Remove exit message heading on third refresh + if [[ "$count" -le 2 ]] && [[ "$*" != *"-e"* ]]; then + echo -e " ${COL_LIGHT_GREEN}Pi-hole Chronometer${COL_NC} + $num_str + ${COL_LIGHT_RED}Press Ctrl-C to exit${COL_NC} + ${COL_DARK_GRAY}$scr_line_str${COL_NC}" + else + echo -e "|¯¯¯(¯)_|¯|_ ___|¯|___$phc_ver_str +| ¯_/¯|_| ' \\/ _ \\ / -_)$lte_ver_str +|_| |_| |_||_\\___/_\\___|$ftl_ver_str + ${COL_DARK_GRAY}$scr_line_str${COL_NC}" + fi + + printFunc " Hostname: " "$sys_name" "$host_info" + printFunc " Uptime: " "$sys_uptime" "$sys_info" + printFunc " Task Load: " "$sys_loadavg" "$sys_info2" + printFunc " CPU usage: " "$cpu_perc%" "$cpu_info" + 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 "Local Qrys: " "$queries_cached_percentage%" "$dns_info" + + printFunc " Blocked: " "$recent_blocked" + printFunc "Top Advert: " "$top_ad" + + # Provide more stats on screens with more lines + if [[ "$scr_lines" -eq 17 ]]; then + if [[ "$DHCP_ACTIVE" == "true" ]]; then + printFunc "Top Domain: " "$top_domain" "last" + else + print_client="true" + fi + else + print_client="true" + fi + + if [[ -n "$print_client" ]]; then + printFunc "Top Domain: " "$top_domain" + printFunc "Top Client: " "$top_client" "last" + fi + + # Handle exit/refresh options + if [[ "$*" == *"-e"* ]]; then exit 0 else - if [[ -n "$1" ]]; then - sleep "${1}" + if [[ "$*" == *"-r"* ]]; then + sleep "$num" else sleep 5 fi @@ -409,14 +547,14 @@ helpFunc() { echo "Usage: pihole -c [options] Example: 'pihole -c -j' 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 -h, --help Display this help text" fi - + exit 0 } @@ -428,8 +566,8 @@ for var in "$@"; do case "$var" in "-j" | "--json" ) jsonFunc;; "-h" | "--help" ) helpFunc;; - "-r" | "--refresh" ) chronoFunc "$2";; - "-e" | "--exit" ) chronoFunc "exit";; + "-r" | "--refresh" ) chronoFunc "$@";; + "-e" | "--exit" ) chronoFunc "$@";; * ) helpFunc "?";; esac done diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index 308e1f5e..72250afd 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -19,11 +19,14 @@ addmode=true verbose=true domList=() -domToRemoveList=() listMain="" listAlt="" +colfile="/opt/pihole/COL_TABLE" +source ${colfile} + + helpFunc() { if [[ "${listMain}" == "${whitelist}" ]]; then param="w" @@ -45,7 +48,8 @@ Options: -nr, --noreload Update ${type}list without refreshing dnsmasq -q, --quiet Make output less verbose -h, --help Show this help dialog - -l, --list Display all your ${type}listed domains" + -l, --list Display all your ${type}listed domains + --nuke Removes all entries in a list" exit 0 } @@ -58,15 +62,19 @@ EscapeRegexp() { } HandleOther() { - # First, convert everything to lowercase - domain=$(sed -e "y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/" <<< "$1") + # Convert to lowercase + domain="${1,,}" # Check validity of domain - validDomain=$(echo "${domain}" | perl -lne 'print if /(?!.*[^a-z0-9-\.].*)^((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9-]+\.)*[a-z]{2,63}/') - if [[ -z "${validDomain}" ]]; then - echo "::: $1 is not a valid argument or domain name" - else + 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 + fi + + if [[ -n "${validDomain}" ]]; then domList=("${domList[@]}" ${validDomain}) + else + echo -e " ${CROSS} ${domain} is not a valid argument or domain name!" fi } @@ -94,7 +102,13 @@ AddDomain() { list="$2" domain=$(EscapeRegexp "$1") + [[ "${list}" == "${whitelist}" ]] && listname="whitelist" + [[ "${list}" == "${blacklist}" ]] && listname="blacklist" + [[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist" + if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then + [[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only" + [[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only" bool=true # Is the domain in the list we want to add it to? grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false @@ -102,101 +116,122 @@ AddDomain() { if [[ "${bool}" == false ]]; then # Domain not found in the whitelist file, add it! if [[ "${verbose}" == true ]]; then - echo "::: Adding $1 to $list..." + echo -e " ${INFO} Adding $1 to $listname..." fi reload=true # Add it to the list we want to add it to echo "$1" >> "${list}" else if [[ "${verbose}" == true ]]; then - echo "::: ${1} already exists in ${list}, no need to add!" + echo -e " ${INFO} ${1} already exists in ${listname}, no need to add!" fi fi elif [[ "${list}" == "${wildcardlist}" ]]; then source "${piholeDir}/setupVars.conf" - # Remove the /* from the end of the IPv4addr. + # Remove the /* from the end of the IP addresses IPV4_ADDRESS=${IPV4_ADDRESS%/*} - IPV6_ADDRESS=${IPV6_ADDRESS} - + IPV6_ADDRESS=${IPV6_ADDRESS%/*} + [[ -z "${type}" ]] && type="--wildcard-only" bool=true # Is the domain in the list? grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false if [[ "${bool}" == false ]]; then if [[ "${verbose}" == true ]]; then - echo "::: Adding $1 to wildcard blacklist..." + echo -e " ${INFO} Adding $1 to wildcard blacklist..." fi - reload=true + reload="restart" echo "address=/$1/${IPV4_ADDRESS}" >> "${wildcardlist}" if [[ "${#IPV6_ADDRESS}" > 0 ]]; then echo "address=/$1/${IPV6_ADDRESS}" >> "${wildcardlist}" fi else if [[ "${verbose}" == true ]]; then - echo "::: ${1} already exists in wildcard blacklist, no need to add!" + echo -e " ${INFO} ${1} already exists in wildcard blacklist, no need to add!" fi fi fi } RemoveDomain() { - list="$2" - domain=$(EscapeRegexp "$1") + list="$2" + domain=$(EscapeRegexp "$1") - if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then - bool=true - # Is it in the list? Logic follows that if its whitelisted it should not be blacklisted and vice versa - grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false - if [[ "${bool}" == true ]]; then - # Remove it from the other one - echo "::: Removing $1 from $list..." - # /I flag: search case-insensitive - sed -i "/${domain}/Id" "${list}" - reload=true - else - if [[ "${verbose}" == true ]]; then - echo "::: ${1} does not exist in ${list}, no need to remove!" - fi - fi - elif [[ "${list}" == "${wildcardlist}" ]]; then - bool=true - # Is it in the list? - grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false - if [[ "${bool}" == true ]]; then - # Remove it from the other one - echo "::: Removing $1 from $list..." - # /I flag: search case-insensitive - sed -i "/address=\/${domain}/Id" "${list}" - reload=true - else - if [[ "${verbose}" == true ]]; then - echo "::: ${1} does not exist in ${list}, no need to remove!" - fi + [[ "${list}" == "${whitelist}" ]] && listname="whitelist" + [[ "${list}" == "${blacklist}" ]] && listname="blacklist" + [[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist" + + if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then + bool=true + [[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only" + [[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only" + # Is it in the list? Logic follows that if its whitelisted it should not be blacklisted and vice versa + grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false + if [[ "${bool}" == true ]]; then + # Remove it from the other one + echo -e " ${INFO} Removing $1 from $listname..." + # /I flag: search case-insensitive + sed -i "/${domain}/Id" "${list}" + reload=true + else + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!" fi fi + elif [[ "${list}" == "${wildcardlist}" ]]; then + [[ -z "${type}" ]] && type="--wildcard-only" + bool=true + # Is it in the list? + grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false + if [[ "${bool}" == true ]]; then + # Remove it from the other one + echo -e " ${INFO} Removing $1 from $listname..." + # /I flag: search case-insensitive + sed -i "/address=\/${domain}/Id" "${list}" + reload=true + else + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!" + fi + fi + fi } +# Update Gravity Reload() { - # Reload hosts file - pihole -g -sd + echo "" + pihole -g --skip-download "${type:-}" } Displaylist() { - if [[ "${listMain}" == "${whitelist}" ]]; then - string="gravity resistant domains" + if [[ -f ${listMain} ]]; then + if [[ "${listMain}" == "${whitelist}" ]]; then + string="gravity resistant domains" + else + string="domains caught in the sinkhole" + fi + verbose=false + echo -e "Displaying $string:\n" + count=1 + while IFS= read -r RD; do + echo " ${count}: ${RD}" + count=$((count+1)) + done < "${listMain}" else - string="domains caught in the sinkhole" + echo -e " ${COL_LIGHT_RED}${listMain} does not exist!${COL_NC}" fi - verbose=false - echo -e "Displaying $string:\n" - count=1 - while IFS= read -r RD; do - echo "${count}: ${RD}" - count=$((count+1)) - done < "${listMain}" exit 0; } +NukeList() { + if [[ -f "${listMain}" ]]; then + # Back up original list + cp "${listMain}" "${listMain}.bck~" + # Empty out file + echo "" > "${listMain}" + fi +} + for var in "$@"; do case "${var}" in "-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";; @@ -204,10 +239,10 @@ for var in "$@"; do "-wild" | "wildcard" ) listMain="${wildcardlist}";; "-nr"| "--noreload" ) reload=false;; "-d" | "--delmode" ) addmode=false;; - "-f" | "--force" ) force=true;; "-q" | "--quiet" ) verbose=false;; "-h" | "--help" ) helpFunc;; "-l" | "--list" ) Displaylist;; + "--nuke" ) NukeList;; * ) HandleOther "${var}";; esac done @@ -220,6 +255,7 @@ fi PoplistFile -if ${reload}; then - Reload +if [[ "${reload}" != false ]]; then + # Ensure that "restart" is used for Wildcard updates + Reload "${reload}" fi diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index e2c0ab11..9e97c69c 100644 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -3,13 +3,14 @@ # (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" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" +PH_TEST="true" +source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" # webInterfaceGitUrl set in basic-install.sh # webInterfaceDir set in basic-install.sh @@ -20,9 +21,100 @@ PH_TEST="true" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" source "${setupVars}" update="false" -# Colour codes -red="\e[1;31m" -def="\e[0m" +coltable="/opt/pihole/COL_TABLE" +source ${coltable} + +check_download_exists() { + status=$(curl --head --silent "https://ftl.pi-hole.net/${1}" | head -n 1) + if grep -q "404" <<< "$status"; then + return 1 + else + return 0 + fi +} + +FTLinstall() { + # Download and install FTL binary + local binary + binary="${1}" + local path + path="${2}" + local str + str="Installing FTL" + echo -ne " ${INFO} ${str}..." + + if curl -sSL --fail "https://ftl.pi-hole.net/${path}" -o "/tmp/${binary}"; then + # Get sha1 of the binary we just downloaded for verification. + curl -sSL --fail "https://ftl.pi-hole.net/${path}.sha1" -o "/tmp/${binary}.sha1" + # Check if we just downloaded text, or a binary file. + cd /tmp || return 1 + if sha1sum --status --quiet -c "${binary}".sha1; then + echo -n "transferred... " + stop_service pihole-FTL &> /dev/null + install -T -m 0755 "/tmp/${binary}" "/usr/bin/pihole-FTL" + rm "/tmp/${binary}" "/tmp/${binary}.sha1" + start_service pihole-FTL &> /dev/null + echo -e "${OVER} ${TICK} ${str}" + return 0 + else + echo -e "${OVER} ${CROSS} ${str}" + echo -e " ${COL_LIGHT_RED}Error: Download of binary from ftl.pi-hole.net failed${COL_NC}" + return 1 + fi + else + echo -e "${OVER} ${CROSS} ${str}" + echo -e " ${COL_LIGHT_RED}Error: URL not found${COL_NC}" + fi +} + +get_binary_name() { + local machine + machine=$(uname -m) + + local str + str="Detecting architecture" + echo -ne " ${INFO} ${str}..." + if [[ "${machine}" == "arm"* || "${machine}" == *"aarch"* ]]; then + # ARM + local rev + rev=$(uname -m | sed "s/[^0-9]//g;") + local lib + lib=$(ldd /bin/ls | grep -E '^\s*/lib' | awk '{ print $1 }') + if [[ "${lib}" == "/lib/ld-linux-aarch64.so.1" ]]; then + echo -e "${OVER} ${TICK} Detected ARM-aarch64 architecture" + binary="pihole-FTL-aarch64-linux-gnu" + elif [[ "${lib}" == "/lib/ld-linux-armhf.so.3" ]]; then + if [[ "$rev" -gt "6" ]]; then + echo -e "${OVER} ${TICK} Detected ARM-hf architecture (armv7+)" + binary="pihole-FTL-arm-linux-gnueabihf" + else + echo -e "${OVER} ${TICK} Detected ARM-hf architecture (armv6 or lower) Using ARM binary" + binary="pihole-FTL-arm-linux-gnueabi" + fi + else + echo -e "${OVER} ${TICK} Detected ARM architecture" + binary="pihole-FTL-arm-linux-gnueabi" + fi + elif [[ "${machine}" == "ppc" ]]; then + # PowerPC + echo -e "${OVER} ${TICK} Detected PowerPC architecture" + binary="pihole-FTL-powerpc-linux-gnu" + elif [[ "${machine}" == "x86_64" ]]; then + # 64bit + echo -e "${OVER} ${TICK} Detected x86_64 architecture" + binary="pihole-FTL-linux-x86_64" + else + # Something else - we try to use 32bit executable and warn the user + if [[ ! "${machine}" == "i686" ]]; then + echo -e "${OVER} ${CROSS} ${str}... + ${COL_LIGHT_RED}Not able to detect architecture (unknown: ${machine}), trying 32bit executable + Contact support if you experience issues (e.g: FTL not running)${COL_NC}" + else + echo -e "${OVER} ${TICK} Detected 32bit (i686) architecture" + fi + binary="pihole-FTL-linux-x86_32" + fi +} fully_fetch_repo() { # Add upstream branches to shallow clone @@ -40,61 +132,78 @@ fully_fetch_repo() { get_available_branches() { # Return available branches - local directory="${1}" + local directory + directory="${1}" + local output cd "${directory}" || return 1 - # Get reachable remote branches - git remote show origin | grep 'tracked' | sed 's/tracked//;s/ //g' + # Get reachable remote branches, but store STDERR as STDOUT variable + output=$( { git remote show origin | grep 'tracked' | sed 's/tracked//;s/ //g'; } 2>&1 ) + echo "$output" return } - fetch_checkout_pull_branch() { # Check out specified branch - local directory="${1}" - local branch="${2}" + local directory + directory="${1}" + local branch + branch="${2}" # Set the reference for the requested branch, fetch, check it put and pull it - cd "${directory}" + cd "${directory}" || return 1 git remote set-branches origin "${branch}" || return 1 git stash --all --quiet &> /dev/null || true - git clean --force -d || true + git clean --quiet --force -d || true git fetch --quiet || return 1 checkout_pull_branch "${directory}" "${branch}" || return 1 } checkout_pull_branch() { # Check out specified branch - local directory="${1}" - local branch="${2}" + local directory + directory="${1}" + local branch + branch="${2}" local oldbranch cd "${directory}" || return 1 oldbranch="$(git symbolic-ref HEAD)" - git checkout "${branch}" || return 1 + str="Switching to branch: '${branch}' from '${oldbranch}'" + echo -ne " ${INFO} $str" + git checkout "${branch}" --quiet || return 1 + echo -e "${OVER} ${TICK} $str" - if [ "$(git diff "${oldbranch}" | grep -c "^")" -gt "0" ]; then + + if [[ "$(git diff "${oldbranch}" | grep -c "^")" -gt "0" ]]; then update="true" fi - git pull || return 1 + git_pull=$(git pull || return 1) + + if [[ "$git_pull" == *"up-to-date"* ]]; then + echo -e " ${INFO} ${git_pull}" + else + echo -e "$git_pull\\n" + fi + return 0 } warning1() { echo " Please note that changing branches severely alters your Pi-hole subsystems" echo " Features that work on the master branch, may not on a development branch" - echo -e " ${red}This feature is NOT supported unless a Pi-hole developer explicitly asks!${def}" + echo -e " ${COL_LIGHT_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}" read -r -p " Have you read and understood this? [y/N] " response - case ${response} in + case "${response}" in [yY][eE][sS]|[yY]) - echo "::: Continuing with branch change." + echo "" return 0 ;; *) - echo "::: Branch change has been cancelled." + echo -e "\\n ${INFO} Branch change has been cancelled" return 1 ;; esac @@ -107,24 +216,23 @@ checkout() { # Avoid globbing set -f - #This is unlikely + # This is unlikely if ! is_repo "${PI_HOLE_FILES_DIR}" ; then - echo "::: Critical Error: Core Pi-hole repo is missing from system!" - echo "::: Please re-run install script from https://github.com/pi-hole/pi-hole" + echo -e " ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system! + Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}" exit 1; fi - if [[ ${INSTALL_WEB} == "true" ]]; then + if [[ "${INSTALL_WEB}" == "true" ]]; then if ! is_repo "${webInterfaceDir}" ; then - echo "::: Critical Error: Web Admin repo is missing from system!" - echo "::: Please re-run install script from https://github.com/pi-hole/pi-hole" + echo -e " ${COL_LIGHT_RED}Error: Web Admin repo is missing from system! + Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}" exit 1; fi fi if [[ -z "${1}" ]]; then - echo "::: No option detected. Please use 'pihole checkout '." - echo "::: Or enter the repository and branch you would like to check out:" - echo "::: 'pihole checkout '" + echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC} + Try 'pihole checkout --help' for more information." exit 1 fi @@ -134,72 +242,117 @@ checkout() { if [[ "${1}" == "dev" ]] ; then # Shortcut to check out development branches - echo "::: Shortcut \"dev\" detected - checking out development / devel branches ..." - echo "::: Pi-hole core" - fetch_checkout_pull_branch "${PI_HOLE_FILES_DIR}" "development" || { echo "Unable to pull Core developement branch"; exit 1; } - if [[ ${INSTALL_WEB} == "true" ]]; then - echo "::: Web interface" - fetch_checkout_pull_branch "${webInterfaceDir}" "devel" || { echo "Unable to pull Web development branch"; exit 1; } + 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; } + if [[ "${INSTALL_WEB}" == "true" ]]; then + echo "" + echo -e " ${INFO} Web interface" + fetch_checkout_pull_branch "${webInterfaceDir}" "devel" || { echo " ${CROSS} Unable to pull Web development branch"; exit 1; } fi - echo "::: done!" + #echo -e " ${TICK} Pi-hole Core" + + get_binary_name + local path + path="development/${binary}" + FTLinstall "${binary}" "${path}" elif [[ "${1}" == "master" ]] ; then # Shortcut to check out master branches - echo "::: Shortcut \"master\" detected - checking out master branches ..." - echo "::: Pi-hole core" - fetch_checkout_pull_branch "${PI_HOLE_FILES_DIR}" "master" || { echo "Unable to pull Core master branch"; exit 1; } + echo -e " ${INFO} Shortcut \"master\" detected - checking out master branches..." + echo -e " ${INFO} Pi-hole core" + fetch_checkout_pull_branch "${PI_HOLE_FILES_DIR}" "master" || { echo " ${CROSS} Unable to pull Core master branch"; exit 1; } if [[ ${INSTALL_WEB} == "true" ]]; then - echo "::: Web interface" - fetch_checkout_pull_branch "${webInterfaceDir}" "master" || { echo "Unable to pull web master branch"; exit 1; } + echo -e " ${INFO} Web interface" + fetch_checkout_pull_branch "${webInterfaceDir}" "master" || { echo " ${CROSS} Unable to pull Web master branch"; exit 1; } fi - echo "::: done!" + #echo -e " ${TICK} Web Interface" + get_binary_name + local path + path="master/${binary}" + FTLinstall "${binary}" "${path}" elif [[ "${1}" == "core" ]] ; then - echo -n "::: Fetching remote branches for Pi-hole core from ${piholeGitUrl} ... " + str="Fetching branches from ${piholeGitUrl}" + echo -ne " ${INFO} $str" if ! fully_fetch_repo "${PI_HOLE_FILES_DIR}" ; then - echo "::: Fetching all branches for Pi-hole core repo failed!" + echo -e "${OVER} ${CROSS} $str" exit 1 fi corebranches=($(get_available_branches "${PI_HOLE_FILES_DIR}")) - echo " done!" - echo "::: ${#corebranches[@]} branches available" - echo ":::" - # Have to user chosing the branch he wants + + if [[ "${corebranches[*]}" == *"master"* ]]; then + echo -e "${OVER} ${TICK} $str + ${INFO} ${#corebranches[@]} branches available for Pi-hole Core" + else + # Print STDERR output from get_available_branches + echo -e "${OVER} ${CROSS} $str\\n\\n${corebranches[*]}" + exit 1 + fi + + echo "" + # Have the user choose the branch they want if ! (for e in "${corebranches[@]}"; do [[ "$e" == "${2}" ]] && exit 0; done); then - echo "::: Requested branch \"${2}\" is not available!" - echo "::: Available branches for core are:" - for e in "${corebranches[@]}"; do echo "::: $e"; done + echo -e " ${INFO} Requested branch \"${2}\" is not available" + echo -e " ${INFO} Available branches for Core are:" + for e in "${corebranches[@]}"; do echo " - $e"; done exit 1 fi checkout_pull_branch "${PI_HOLE_FILES_DIR}" "${2}" - elif [[ "${1}" == "web" && "${INSTALL_WEB}" == "true" ]] ; then - echo -n "::: Fetching remote branches for the web interface from ${webInterfaceGitUrl} ... " + elif [[ "${1}" == "web" ]] && [[ "${INSTALL_WEB}" == "true" ]] ; then + str="Fetching branches from ${webInterfaceGitUrl}" + echo -ne " ${INFO} $str" if ! fully_fetch_repo "${webInterfaceDir}" ; then - echo "::: Fetching all branches for Pi-hole web interface repo failed!" + echo -e "${OVER} ${CROSS} $str" exit 1 fi webbranches=($(get_available_branches "${webInterfaceDir}")) - echo " done!" - echo "::: ${#webbranches[@]} branches available" - echo ":::" - # Have to user chosing the branch he wants + + if [[ "${webbranches[*]}" == *"master"* ]]; then + echo -e "${OVER} ${TICK} $str + ${INFO} ${#webbranches[@]} branches available for Web Admin" + else + # Print STDERR output from get_available_branches + echo -e "${OVER} ${CROSS} $str\\n\\n${webbranches[*]}" + exit 1 + fi + + echo "" + # Have the user choose the branch they want if ! (for e in "${webbranches[@]}"; do [[ "$e" == "${2}" ]] && exit 0; done); then - echo "::: Requested branch \"${2}\" is not available!" - echo "::: Available branches for web are:" - for e in "${webbranches[@]}"; do echo "::: $e"; done + echo -e " ${INFO} Requested branch \"${2}\" is not available" + echo -e " ${INFO} Available branches for Web Admin are:" + for e in "${webbranches[@]}"; do echo " - $e"; done exit 1 fi checkout_pull_branch "${webInterfaceDir}" "${2}" + elif [[ "${1}" == "ftl" ]] ; then + get_binary_name + local path + path="${2}/${binary}" + + if check_download_exists "$path"; then + echo " ${TICK} Branch ${2} exists" + FTLinstall "${binary}" "${path}" + 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}') ) + echo -e " ${INFO} Available branches for FTL are:" + for e in "${ftlbranches[@]}"; do echo " - $e"; done + exit 1 + fi + else - echo "::: Requested option \"${1}\" is not available!" + echo -e " ${INFO} Requested option \"${1}\" is not available" exit 1 fi # Force updating everything - if [[ ! "${1}" == "web" && "${update}" == "true" ]]; then - echo "::: Running installer to upgrade your installation" + if [[ ( ! "${1}" == "web" && ! "${1}" == "ftl" ) && "${update}" == "true" ]]; then + echo -e " ${INFO} Running installer to upgrade your installation" if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then exit 0 else - echo "Unable to complete update, contact Pi-hole" + echo -e " ${COL_LIGHT_RED} Error: Unable to complete update, please contact support${COL_NC}" exit 1 fi fi diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index 8020cc80..d69c5e4d 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -9,534 +9,1156 @@ # Please see LICENSE file for your rights under this license. - +# -e option instructs bash to immediately exit if any command [1] has a non-zero exit status +# -u a reference to any variable you haven't previously defined +# with the exceptions of $* and $@ - is an error, and causes the program to immediately exit +# -o pipefail prevents errors in a pipeline from being masked. If any command in a pipeline fails, +# that return code will be used as the return code of the whole pipeline. By default, the +# pipeline's return code is that of the last command - even if it succeeds set -o pipefail +#IFS=$'\n\t' ######## GLOBAL VARS ######## -VARSFILE="/etc/pihole/setupVars.conf" -DEBUG_LOG="/var/log/pihole_debug.log" -DNSMASQFILE="/etc/dnsmasq.conf" -DNSMASQCONFDIR="/etc/dnsmasq.d/*" -LIGHTTPDFILE="/etc/lighttpd/lighttpd.conf" -LIGHTTPDERRFILE="/var/log/lighttpd/error.log" -GRAVITYFILE="/etc/pihole/gravity.list" -WHITELISTFILE="/etc/pihole/whitelist.txt" -BLACKLISTFILE="/etc/pihole/blacklist.txt" -ADLISTFILE="/etc/pihole/adlists.list" -PIHOLELOG="/var/log/pihole.log" -PIHOLEGITDIR="/etc/.pihole/" -ADMINGITDIR="/var/www/html/admin/" -WHITELISTMATCHES="/tmp/whitelistmatches.list" -readonly FTLLOG="/var/log/pihole-FTL.log" +# These variables would normally be next to the other files +# but we need them to be first in order to get the colors needed for the script output +PIHOLE_SCRIPTS_DIRECTORY="/opt/pihole" +PIHOLE_COLTABLE_FILE="${PIHOLE_SCRIPTS_DIRECTORY}/COL_TABLE" -TIMEOUT=60 -# Header info and introduction -cat << EOM -::: Beginning Pi-hole debug at $(date)! -::: -::: This process collects information from your Pi-hole, and optionally uploads -::: it to a unique and random directory on tricorder.pi-hole.net. -::: -::: NOTE: All log files auto-delete after 48 hours and ONLY the Pi-hole developers -::: can access your data via the given token. We have taken these extra steps to -::: secure your data and will work to further reduce any personal information gathered. -::: -::: Please read and note any issues, and follow any directions advised during this process. -EOM +# These provide the colors we need for making the log more readable +if [[ -f ${PIHOLE_COLTABLE_FILE} ]]; then + source ${PIHOLE_COLTABLE_FILE} +else + COL_NC='\e[0m' # No Color + COL_RED='\e[1;91m' + COL_GREEN='\e[1;32m' + COL_YELLOW='\e[1;33m' + COL_PURPLE='\e[1;35m' + COL_CYAN='\e[0;36m' + TICK="[${COL_GREEN}✓${COL_NC}]" + CROSS="[${COL_RED}✗${COL_NC}]" + INFO="[i]" + OVER="\r\033[K" +fi -source ${VARSFILE} +OBFUSCATED_PLACEHOLDER="" -### Private functions exist here ### -log_write() { - echo "${@}" >&3 +# 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_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 +CORE_GIT_DIRECTORY="/etc/.pihole" +CRON_D_DIRECTORY="/etc/cron.d" +DNSMASQ_D_DIRECTORY="/etc/dnsmasq.d" +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" +WEB_SERVER_CONFIG_DIRECTORY="/etc/lighttpd" +HTML_DIRECTORY="/var/www/html" +WEB_GIT_DIRECTORY="${HTML_DIRECTORY}/admin" +BLOCK_PAGE_DIRECTORY="${HTML_DIRECTORY}/pihole" + +# 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" + +PIHOLE_DEFAULT_AD_LISTS="${PIHOLE_DIRECTORY}/adlists.default" +PIHOLE_USER_DEFINED_AD_LISTS="${PIHOLE_DIRECTORY}/adlists.list" +PIHOLE_BLACKLIST_FILE="${PIHOLE_DIRECTORY}/blacklist.txt" +PIHOLE_BLOCKLIST_FILE="${PIHOLE_DIRECTORY}/gravity.list" +PIHOLE_INSTALL_LOG_FILE="${PIHOLE_DIRECTORY}/install.log" +PIHOLE_RAW_BLOCKLIST_FILES=${PIHOLE_DIRECTORY}/list.* +PIHOLE_LOCAL_HOSTS_FILE="${PIHOLE_DIRECTORY}/local.list" +PIHOLE_LOGROTATE_FILE="${PIHOLE_DIRECTORY}/logrotate" +PIHOLE_SETUP_VARS_FILE="${PIHOLE_DIRECTORY}/setupVars.conf" +PIHOLE_WHITELIST_FILE="${PIHOLE_DIRECTORY}/whitelist.txt" + +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_DEBUG_LOG_SANITIZED="${LOG_DIRECTORY}/pihole_debug-sanitized.log" +PIHOLE_FTL_LOG="${LOG_DIRECTORY}/pihole-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" + +# 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") + +# Store Pi-hole's processes in an array for easy use and parsing +PIHOLE_PROCESSES=( "dnsmasq" "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 +mapfile -t array <<< "$var" +REQUIRED_FILES=(${PIHOLE_CRON_FILE} +${PIHOLE_DNS_CONFIG_FILE} +${PIHOLE_DHCP_CONFIG_FILE} +${PIHOLE_WILDCARD_CONFIG_FILE} +${WEB_SERVER_CONFIG_FILE} +${PIHOLE_DEFAULT_AD_LISTS} +${PIHOLE_USER_DEFINED_AD_LISTS} +${PIHOLE_BLACKLIST_FILE} +${PIHOLE_BLOCKLIST_FILE} +${PIHOLE_INSTALL_LOG_FILE} +${PIHOLE_RAW_BLOCKLIST_FILES} +${PIHOLE_LOCAL_HOSTS_FILE} +${PIHOLE_LOGROTATE_FILE} +${PIHOLE_SETUP_VARS_FILE} +${PIHOLE_WHITELIST_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}) + +DISCLAIMER="This process collects information from your Pi-hole, and optionally uploads it to a unique and random directory on tricorder.pi-hole.net. + +The intent of this script is to allow users to self-diagnose their installations. This is accomplished by running tests against our software and providing the user with links to FAQ articles when a problem is detected. Since we are a small team and Pi-hole has been growing steadily, it is our hope that this will help us spend more time on development. + +NOTE: All log files auto-delete after 48 hours and ONLY the Pi-hole developers can access your data via the given token. We have taken these extra steps to secure your data and will work to further reduce any personal information gathered. +" + +show_disclaimer(){ + log_write "${DISCLAIMER}" } -log_echo() { - case ${1} in - -n) - echo -n "::: ${2}" - log_write "${2}" - ;; - -r) - echo "::: ${2}" - log_write "${2}" - ;; - -l) - echo "${2}" - log_write "${2}" - ;; - *) - echo "::: ${1}" - log_write "${1}" +source_setup_variables() { + # Display the current test that is running + log_write "\n${COL_PURPLE}*** [ INITIALIZING ]${COL_NC} Sourcing setup variables" + # If the variable file exists, + if ls "${PIHOLE_SETUP_VARS_FILE}" 1> /dev/null 2>&1; then + log_write "${INFO} Sourcing ${PIHOLE_SETUP_VARS_FILE}..."; + # source it + source ${PIHOLE_SETUP_VARS_FILE} + else + # If it can't, show an error + log_write "${PIHOLE_SETUP_VARS_FILE} ${COL_RED}does not exist or cannot be read.${COL_NC}" + fi +} + +make_temporary_log() { + # Create a random temporary file for the log + TEMPLOG=$(mktemp /tmp/pihole_temp.XXXXXX) + # Open handle 3 for templog + # https://stackoverflow.com/questions/18460186/writing-outputs-to-log-file-and-console + exec 3>"$TEMPLOG" + # Delete templog, but allow for addressing via file handle + # This lets us write to the log without having a temporary file on the drive, which + # is meant to be a security measure so there is not a lingering file on the drive during the debug process + rm "$TEMPLOG" +} + +log_write() { + # echo arguments to both the log and the console + echo -e "${@}" | tee -a /proc/$$/fd/3 +} + +copy_to_debug_log() { + # Copy the contents of file descriptor 3 into the debug log + cat /proc/$$/fd/3 > "${PIHOLE_DEBUG_LOG}" + # Since we use color codes such as '\e[1;33m', they should be removed before being + # uploaded to our server, since it can't properly display in color + # This is accomplished by use sed to remove characters matching that patter + # The entire file is then copied over to a sanitized version of the log + sed 's/\[[0-9;]\{1,5\}m//g' > "${PIHOLE_DEBUG_LOG_SANITIZED}" <<< cat "${PIHOLE_DEBUG_LOG}" +} + +initiate_debug() { + # Clear the screen so the debug log is readable + clear + show_disclaimer + # Display that the debug process is beginning + 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 initiated." +} + +# This is a function for visually displaying the curent test that is being run. +# Accepts one variable: the name of what is being diagnosed +# Colors do not show in the dasboard, but the icons do: [i], [✓], and [✗] +echo_current_diagnostic() { + # Colors are used for visually distinguishing each test in the output + # These colors do not show in the GUI, but the formatting will + log_write "\n${COL_PURPLE}*** [ DIAGNOSING ]:${COL_NC} ${1}" +} + +compare_local_version_to_git_version() { + # The git directory to check + 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 + 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 + local error_msg="git status failed" + # If the pihole git directory exists, + if [[ -d "${git_dir}" ]]; then + # move into it + cd "${git_dir}" || \ + # If not, show an error + 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); + # What branch they are on + local remote_branch + remote_branch=$(git rev-parse --abbrev-ref HEAD); + # The commit they are on + local remote_commit + remote_commit=$(git describe --long --dirty --tags --always) + # 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 + + # If the repo is on the master branch, they are on the stable codebase + if [[ "${remote_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 + 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})" + fi + # echo the current commit + log_write "${INFO} Commit: ${remote_commit}" + # If git status failed, + else + # Return an error message + log_write "${error_msg}" + # and exit with a non zero code + return 1 + fi + else + : + fi +} + +check_ftl_version() { + local ftl_name="FTL" + echo_current_diagnostic "${ftl_name} 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}" + 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})" + fi +} + +# Checks the core version of the Pi-hole codebase +check_component_versions() { + # Check the Web version, branch, and commit + compare_local_version_to_git_version "${CORE_GIT_DIRECTORY}" "Core" + # Check the Web version, branch, and commit + compare_local_version_to_git_version "${WEB_GIT_DIRECTORY}" "Web" + # Check the FTL version + check_ftl_version +} + + +get_program_version() { + local program_name="${1}" + # Create a loval 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 + case "${program_name}" in + "lighttpd") program_version="$(${program_name} -v |& head -n1 | cut -d '/' -f2 | cut -d ' ' -f1)" + ;; + "dnsmasq") program_version="$(${program_name} -v |& head -n1 | awk '{print $3}')" + ;; + "php") program_version="$(${program_name} -v |& head -n1 | cut -d '-' -f1 | cut -d ' ' -f2)" + ;; + # If a match is not found, show an error + *) echo "Unrecognized program"; + esac + # If the program does not have a version (the variable is empty) + if [[ -z "${program_version}" ]]; then + # Display and error + log_write "${CROSS} ${COL_RED}${program_name} version could not be detected.${COL_NC}" + else + # Otherwise, display the version + log_write "${INFO} ${program_version}" + fi +} + +# These are the most critical dependencies of Pi-hole, so we check for them +# and their versions, using the functions above. +check_critical_program_versions() { + # Use the function created earlier and bundle them into one function that checks all the version numbers + get_program_version "dnsmasq" + get_program_version "lighttpd" + get_program_version "php" +} + +is_os_supported() { + local os_to_check="${1}" + # Strip just the base name of the system using sed + 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 } -header_write() { - log_echo "" - log_echo "---= ${1}" - log_write "" -} +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 + IFS=$'\r\n' command eval 'distro_info=( $(cat /etc/*release) )' -file_parse() { - while read -r line; do - if [ ! -z "${line}" ]; then - [[ "${line}" =~ ^#.*$ || ! "${line}" || "${line}" == "WEBPASSWORD="* ]] && continue - log_write "${line}" - fi - done < "${1}" - log_write "" -} - -block_parse() { - log_write "${1}" -} - -lsof_parse() { - local user - local process - - user=$(echo ${1} | cut -f 3 -d ' ' | cut -c 2-) - process=$(echo ${1} | cut -f 2 -d ' ' | cut -c 2-) - [[ ${2} -eq ${process} ]] \ - && echo "::: Correctly configured." \ - || log_echo "::: Failure: Incorrectly configured daemon." - - log_write "Found user ${user} with process ${process}" -} - - -version_check() { - header_write "Detecting Installed Package Versions:" - - local error_found - local pi_hole_ver - local pi_hole_branch - local pi_hole_commit - local admin_ver - local admin_branch - local admin_commit - local light_ver - local php_ver - local status - error_found=0 - - cd "${PIHOLEGITDIR}" &> /dev/null || \ - { status="Pi-hole git directory not found."; error_found=1; } - if git status &> /dev/null; then - pi_hole_ver=$(git describe --tags --abbrev=0) - pi_hole_branch=$(git rev-parse --abbrev-ref HEAD) - pi_hole_commit=$(git describe --long --dirty --tags --always) - log_echo -r "Pi-hole: ${pi_hole_ver:-Untagged} (${pi_hole_branch:-Detached}:${pi_hole_commit})" - else - status=${status:-"Pi-hole repository damaged."} - error_found=1 - fi - if [[ "${status}" ]]; then - log_echo "${status}" - unset status - fi - - cd "${ADMINGITDIR}" || \ - { status="Pi-hole Dashboard git directory not found."; error_found=1; } - if git status &> /dev/null; then - admin_ver=$(git describe --tags --abbrev=0) - admin_branch=$(git rev-parse --abbrev-ref HEAD) - admin_commit=$(git describe --long --dirty --tags --always) - log_echo -r "Pi-hole Dashboard: ${admin_ver:-Untagged} (${admin_branch:-Detached}:${admin_commit})" - else - status=${status:-"Pi-hole Dashboard repository damaged."} - error_found=1 - fi - if [[ "${status}" ]]; then - log_echo "${status}" - unset status - fi - - if light_ver=$(lighttpd -v |& head -n1 | cut -d " " -f1); then - log_echo -r "${light_ver}" - else - log_echo "lighttpd not installed." - error_found=1 - fi - if php_ver=$(php -v |& head -n1); then - log_echo -r "${php_ver}" - else - log_echo "PHP not installed." - error_found=1 - fi - - return "${error_found}" -} - -dir_check() { - header_write "Detecting contents of ${1}:" - for file in $1*; do - header_write "File ${file} found" - echo -n "::: Parsing..." - file_parse "${file}" - echo "done" + # 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=$(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}" + else + # Since we only need the pretty name, we can just skip over anything that is not a match + : + fi done - echo ":::" + # Set the IFS back to what it was + IFS="$OLD_IFS" } -files_check() { - #Check non-zero length existence of ${1} - header_write "Detecting existence of ${1}:" - local search_file="${1}" - if [[ -s ${search_file} ]]; then - echo -n "::: File exists, parsing..." - file_parse "${search_file}" - echo "done" - return 0 +diagnose_operating_system() { + # error message in a variable so we can easily modify it later (or re-use it) + local error_msg="Distribution unknown -- most likely you are on an unsupported platform and may run into issues." + # Display the current test that is running + echo_current_diagnostic "Operating system" + + # If 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 else - log_echo "${1} not found!" - return 1 + # 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})" fi - echo ":::" } -source_file() { - local file_found=$(files_check "${1}") \ - && (source "${1}" &> /dev/null && echo "${file_found} and was successfully sourced") \ - || log_echo -l "${file_found} and could not be sourced" -} - -distro_check() { - local soft_fail - header_write "Detecting installed OS Distribution" - soft_fail=0 - local distro="$(cat /etc/*release)" && block_parse "${distro}" || (log_echo "Distribution details not found." && soft_fail=1) - return "${soft_fail}" +check_selinux() { + # SELinux is not supported by the Pi-hole + echo_current_diagnostic "SELinux" + # Check if a SELinux configuration file exists + if [[ -f /etc/selinux/config ]]; then + # If a SELinux configuration file was found, check the default SELinux mode. + DEFAULT_SELINUX=$(awk -F= '/^SELINUX=/ {print $2}' /etc/selinux/config) + case "${DEFAULT_SELINUX,,}" in + enforcing) + log_write "${CROSS} ${COL_RED}Default SELinux: $DEFAULT_SELINUX${COL_NC}" + ;; + *) # 'permissive' and 'disabled' + log_write "${TICK} ${COL_GREEN}Default SELinux: $DEFAULT_SELINUX${COL_NC}"; + ;; + esac + # Check the current state of SELinux + CURRENT_SELINUX=$(getenforce) + case "${CURRENT_SELINUX,,}" in + enforcing) + log_write "${CROSS} ${COL_RED}Current SELinux: $CURRENT_SELINUX${COL_NC}" + ;; + *) # 'permissive' and 'disabled' + log_write "${TICK} ${COL_GREEN}Current SELinux: $CURRENT_SELINUX${COL_NC}"; + ;; + esac + else + log_write "${INFO} ${COL_GREEN}SELinux not detected${COL_NC}"; + fi } processor_check() { - header_write "Checking processor variety" - log_write $(uname -m) && return 0 || return 1 -} - -ipv6_check() { - # Check if system is IPv6 enabled, for use in other functions - if [[ $IPV6_ADDRESS ]]; then - ls /proc/net/if_inet6 &>/dev/null - return 0 + echo_current_diagnostic "Processor" + # Store the processor type in a variable + PROCESSOR=$(uname -m) + # If it does not contain a value, + if [[ -z "${PROCESSOR}" ]]; then + # we couldn't detect it, so show an error + PROCESSOR=$(lscpu | awk '/Architecture/ {print $2}') + log_write "${CROSS} ${COL_RED}${PROCESSOR}${COL_NC} has not been tested with FTL, but may still work: (${FAQ_FTL_COMPATIBILITY})" else - return 1 + # Check if the architecture is currently supported for FTL + case "${PROCESSOR}" in + "amd64") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" + ;; + "armv6l") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" + ;; + "armv6") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" + ;; + "armv7l") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" + ;; + "aarch64") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}" + ;; + # Otherwise, show the processor type + *) log_write "${INFO} ${PROCESSOR}"; + esac fi } -ip_check() { - local protocol=${1} - local gravity=${2} - header_write "Checking IPv${protocol} Stack" - - local ip_addr_list="$(ip -${protocol} addr show dev ${PIHOLE_INTERFACE} | awk -F ' ' '{ for(i=1;i<=NF;i++) if ($i ~ '/^inet/') print $(i+1) }')" - if [[ -n ${ip_addr_list} ]]; then - log_write "IPv${protocol} on ${PIHOLE_INTERFACE}" - log_write "Gravity configured for: ${2:-NOT CONFIGURED}" - log_write "----" - log_write "${ip_addr_list}" - echo "::: IPv${protocol} addresses located on ${PIHOLE_INTERFACE}" - ip_ping_check ${protocol} - return $(( 0 + $? )) +parse_setup_vars() { + echo_current_diagnostic "Setup variables" + # If the file exists, + if [[ -r "${PIHOLE_SETUP_VARS_FILE}" ]]; then + # parse it + parse_file "${PIHOLE_SETUP_VARS_FILE}" else - log_echo "No IPv${protocol} found on ${PIHOLE_INTERFACE}" - return 1 + # If not, show an error + log_write "${CROSS} ${COL_RED}Could not read ${PIHOLE_SETUP_VARS_FILE}.${COL_NC}" fi } -ip_ping_check() { - local protocol=${1} - local cmd - - if [[ ${protocol} == "6" ]]; then - cmd="ping6" - g_addr="2001:4860:4860::8888" - else - cmd="ping" - g_addr="8.8.8.8" - fi - - local ip_def_gateway=$(ip -${protocol} route | grep default | cut -d ' ' -f 3) - if [[ -n ${ip_def_gateway} ]]; then - echo -n "::: Pinging default IPv${protocol} gateway: " - if ! ping_gateway="$(${cmd} -q -W 3 -c 3 -n ${ip_def_gateway} -I ${PIHOLE_INTERFACE} | tail -n 3)"; then - log_echo "Gateway did not respond." - return 1 - else - log_echo "Gateway responded." - log_write "${ping_gateway}" - fi - echo -n "::: Pinging Internet via IPv${protocol}: " - if ! ping_inet="$(${cmd} -q -W 3 -c 3 -n ${g_addr} -I ${PIHOLE_INTERFACE} | tail -n 3)"; then - log_echo "Query did not respond." - return 1 - else - log_echo "Query responded." - log_write "${ping_inet}" - fi - else - log_echo " No gateway detected." - fi - return 0 -} - -port_check() { - local lsof_value - - lsof_value=$(lsof -i ${1}:${2} -FcL | tr '\n' ' ') \ - && lsof_parse "${lsof_value}" "${3}" \ - || log_echo "Failure: IPv${1} Port not in use" -} - -daemon_check() { - # Check for daemon ${1} on port ${2} - header_write "Daemon Process Information" - - echo "::: Checking ${2} port for ${1} listener." - - if [[ ${IPV6_READY} ]]; then - port_check 6 "${2}" "${1}" - fi - lsof_value=$(lsof -i 4:${2} -FcL | tr '\n' ' ') \ - port_check 4 "${2}" "${1}" -} - -testResolver() { +does_ip_match_setup_vars() { + # Check for IPv4 or 6 local protocol="${1}" - header_write "Resolver Functions Check (IPv${protocol})" - local IP="${2}" - local g_addr - local l_addr - local url - local testurl - local localdig - local piholedig - local remotedig - - if [[ ${protocol} == "6" ]]; then - g_addr="2001:4860:4860::8888" - l_addr="::1" - r_type="AAAA" - else - g_addr="8.8.8.8" - l_addr="127.0.0.1" - r_type="A" - fi - - # Find a blocked url that has not been whitelisted. - url=$(shuf -n 1 "${GRAVITYFILE}" | awk -F ' ' '{ print $2 }') - - testurl="${url:-doubleclick.com}" - - - log_write "Resolution of ${testurl} from Pi-hole (${l_addr}):" - if localdig=$(dig -"${protocol}" "${testurl}" @${l_addr} +short "${r_type}"); then - log_write "${localdig}" - else - log_write "Failed to resolve ${testurl} on Pi-hole (${l_addr})" - fi - log_write "" - - log_write "Resolution of ${testurl} from Pi-hole (${IP}):" - if piholedig=$(dig -"${protocol}" "${testurl}" @"${IP}" +short "${r_type}"); then - log_write "${piholedig}" - else - log_write "Failed to resolve ${testurl} on Pi-hole (${IP})" - fi - log_write "" - - - log_write "Resolution of ${testurl} from ${g_addr}:" - if remotedig=$(dig -"${protocol}" "${testurl}" @${g_addr} +short "${r_type}"); then - log_write "${remotedig:-NXDOMAIN}" - else - log_write "Failed to resolve ${testurl} on upstream server ${g_addr}" - fi - log_write "" -} - -testChaos(){ - # Check Pi-hole specific records - - log_write "Pi-hole dnsmasq specific records lookups" - log_write "Cache Size:" - log_write $(dig +short chaos txt cachesize.bind) - log_write "Upstream Servers:" - log_write $(dig +short chaos txt servers.bind) - log_write "" - -} -checkProcesses() { - header_write "Processes Check" - - echo "::: Logging status of lighttpd, dnsmasq and pihole-FTL..." - PROCESSES=( lighttpd dnsmasq pihole-FTL ) - for i in "${PROCESSES[@]}"; do - log_write "Status for ${i} daemon:" - log_write $(systemctl is-active "${i}") - done - log_write "" -} - -debugLighttpd() { - echo "::: Checking for necessary lighttpd files." - files_check "${LIGHTTPDFILE}" - files_check "${LIGHTTPDERRFILE}" - echo ":::" -} - -countdown() { - local tuvix - tuvix=${TIMEOUT} - printf "::: Logging will automatically teminate in %s seconds\n" "${TIMEOUT}" - while [ $tuvix -ge 1 ] - do - printf ":::\t%s seconds left. " "${tuvix}" - if [[ -z "${WEBCALL}" ]]; then - printf "\r" + # IP address to check for + local ip_address="${2}" + # See what IP is in the setupVars.conf file + local 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 - printf "\n" + # 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 - sleep 5 - tuvix=$(( tuvix - 5 )) + + 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 + # Get the lines that are in the file(s) and store them in an array for parsing later + declare -a ip_addr_list=( $(ip -${protocol} addr show dev ${PIHOLE_INTERFACE} | awk -F ' ' '{ for(i=1;i<=NF;i++) if ($i ~ '/^inet/') print $(i+1) }') ) + + # If there is something in the IP address list, + if [[ -n ${ip_addr_list} ]]; then + # Local iterator + local i + # Display the protocol and interface + 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]}" + done + # Print a blank line just for formatting + log_write "" + else + # If there are no IPs detected, explain that the protocol is not configured + log_write "${CROSS} ${COL_RED}No IPv${protocol} address(es) found on the ${PIHOLE_INTERFACE}${COL_NC} interace.\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() { + # Give the first argument a readable name (a 4 or a six should be the argument) + local protocol="${1}" + # If the protocol is 6, + if [[ ${protocol} == "6" ]]; then + # use ping6 + cmd="ping6" + # and Google's public IPv6 address + public_address="2001:4860:4860::8888" + else + # Otherwise, just use ping + cmd="ping" + # and Google's public IPv4 address + public_address="8.8.8.8" + fi +} + +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 + 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}" + # Let the user know we will ping the gateway for a response + log_write " * Pinging ${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, + if ! ${cmd} -c 3 -W 2 -n ${gateway} -I ${PIHOLE_INTERFACE} >/dev/null; then + # let the user know + log_write "${CROSS} ${COL_RED}Gateway did not respond.${COL_NC} ($FAQ_GATEWAY)\n" + # and return an error code + return 1 + # Otherwise, + else + # show a success + log_write "${TICK} ${COL_GREEN}Gateway responded.${COL_NC}" + # and return a success code + return 0 + fi + fi +} + +ping_internet() { + local protocol="${1}" + # Ping a public address using the protocol passed as an argument + ping_ipv4_or_ipv6 "${protocol}" + log_write "* Checking Internet connectivity via IPv${protocol}..." + # Try to ping the address 3 times + if ! ${cmd} -W 2 -c 3 -n ${public_address} -I ${PIHOLE_INTERFACE} >/dev/null; then + # if it's unsuccessful, show an error + log_write "${CROSS} ${COL_RED}Cannot reach the Internet.${COL_NC}\n" + return 1 + else + # Otherwise, show success + log_write "${TICK} ${COL_GREEN}Query responded.${COL_NC}\n" + return 0 + fi +} + +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="dnsmasq" + local web_server="lighttpd" + local ftl="pihole-FTL" + if [[ "${service_name}" == "${resolver}" ]] || [[ "${service_name}" == "${web_server}" ]] || [[ "${service_name}" == "${ftl}" ]]; then + # if port 53 is dnsmasq, show it in green as it's standard + log_write "[${COL_GREEN}${port_number}${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})" + fi +} + +check_required_ports() { + echo_current_diagnostic "Ports in use" + # Since Pi-hole needs 53, 80, and 4711, check what they are being used by + # so we can detect any issues + local resolver="dnsmasq" + local web_server="lighttpd" + local ftl="pihole-FTL" + # Create an array for these ports in use + ports_in_use=() + # Sort the addresses and remove duplicates + while IFS= read -r line; do + ports_in_use+=( "$line" ) + done < <( lsof -i -P -n | awk -F' ' '/LISTEN/ {print $9, $1}' | sort -n | uniq | cut -d':' -f2 ) + + # Now that we have the values stored, + for i in "${!ports_in_use[@]}"; do + # loop through them and assign some local variables + local port_number + port_number="$(echo "${ports_in_use[$i]}" | awk '{print $1}')" + local service_name + service_name=$(echo "${ports_in_use[$i]}" | awk '{print $2}') + # Use a case statement to determine if the right services are using the right ports + case "${port_number}" in + 53) compare_port_to_service_assigned "${resolver}" + ;; + 80) compare_port_to_service_assigned "${web_server}" + ;; + 4711) compare_port_to_service_assigned "${ftl}" + ;; + # If it's not a default port that Pi-hole needs, just print it out for the user to see + *) log_write "[${port_number}] is in use by ${service_name}"; + esac done } -# Continuously append the pihole.log file to the pihole_debug.log file -dumpPiHoleLog() { - trap '{ echo -e "\n::: Finishing debug write from interrupt... Quitting!" ; exit 1; }' INT - echo "::: " - echo "::: --= User Action Required =--" - echo -e "::: Try loading a site that you are having trouble with now from a client web browser.. \n:::\t(Press CTRL+C to finish logging.)" - header_write "pihole.log" - if [ -e "${PIHOLELOG}" ]; then - # Dummy process to use for flagging down tail to terminate - countdown & - tail -n0 -f --pid=$! "${PIHOLELOG}" >&4 - else - log_write "No pihole.log file found!" - printf ":::\tNo pihole.log file found!\n" - fi +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 + echo_current_diagnostic "Networking" + detect_ip_addresses "4" + detect_ip_addresses "6" + ping_gateway "4" + ping_gateway "6" + check_required_ports } -# Anything to be done after capturing of pihole.log terminates -finalWork() { - local tricorder - echo "::: Finshed debugging!" +check_x_headers() { + # The X-Headers allow us to determine from the command line if the Web + # lighttpd.conf has a directive to show "X-Pi-hole: A black hole for Internet advertisements." + # in the header of any Pi-holed domain + # 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" + # 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 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." + 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 ${COL_GREEN}${block_page}${COL_NC}" + else + # Otherwise, show an error + log_write "$CROSS ${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 - # Ensure the file exists, create if not, clear if exists. - truncate --size=0 "${DEBUG_LOG}" - chmod 644 ${DEBUG_LOG} - chown "$USER":pihole ${DEBUG_LOG} - # copy working temp file to final log location - cat /proc/$$/fd/3 >> "${DEBUG_LOG}" - # Straight dump of tailing the logs, can sanitize later if needed. - cat /proc/$$/fd/4 >> "${DEBUG_LOG}" + # Same logic applies to the dashbord as above, if the X-Header matches what a working system shoud have, + if [[ $dashboard == "$dashboard_working" ]]; then + # then we can show a success + log_write "$TICK ${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 + log_write "$CROSS ${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 +} - echo "::: The debug log can be uploaded to tricorder.pi-hole.net for sharing with developers only." +dig_at() { + # We need to test if Pi-hole can properly resolve domain names + # as it is an essential piece of the software + + # 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 + # 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" + + # If the protocol (4 or 6) is 6, + if [[ ${protocol} == "6" ]]; then + # Set the IPv6 variables and record type + local local_address="::1" + local pihole_address="${IPV6_ADDRESS%/*}" + local remote_address="2001:4860:4860::8888" + local record_type="AAAA" + # Othwerwise, it should be 4 + else + # so use the IPv4 values + local local_address="127.0.0.1" + local pihole_address="${IPV4_ADDRESS%/*}" + local remote_address="8.8.8.8" + local record_type="A" + fi + + # Find a random blocked url that has not been whitelisted. + # 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=$(shuf -n 1 "${PIHOLE_BLOCKLIST_FILE}" | awk -F ' ' '{ print $2 }') + + # 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 + + # 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 + + # 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 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 + # Otherwise, show an error + log_write "${CROSS} ${COL_RED}Failed to resolve${COL_NC} ${remote_url} via ${COL_RED}a remote, public DNS server${COL_NC} (${remote_address})" + fi +} + +process_status(){ + # Check to make sure Pi-hole's services are running and active + echo_current_diagnostic "Pi-hole processes" + # Local iterator + local i + # For each process, + for i in "${PIHOLE_PROCESSES[@]}"; do + # get its status via systemctl + local status_of_process=$(systemctl is-active "${i}") + # and print it out to the user + if [[ "${status_of_process}" == "active" ]]; then + # If it's active, show it in green + log_write "${TICK} ${COL_GREEN}${i}${COL_NC} daemon is ${COL_GREEN}${status_of_process}${COL_NC}" + else + # If it's not, show it in red + log_write "${CROSS} ${COL_RED}${i}${COL_NC} daemon is ${COL_RED}${status_of_process}${COL_NC}" + fi + done +} + +make_array_from_file() { + local filename="${1}" + # The second argument can put a limit on how many line should be read from the file + # Since some of the files are so large, this is helpful to limit the output + local limit=${2} + # A local iterator for testing if we are at the limit above + local i=0 + # Set the array to be empty so we can start fresh when the function is used + local file_content=() + # If the file is a directory + if [[ -d "${filename}" ]]; then + # do nothing since it cannot be parsed + : + else + # Otherwise, read the file line by line + while IFS= read -r line;do + # Othwerise, strip out comments and blank lines + new_line=$(echo "${line}" | sed -e 's/#.*$//' -e '/^$/d') + # If the line still has content (a non-zero value) + if [[ -n "${new_line}" ]]; then + # Put it into the array + file_content+=("${new_line}") + else + # Otherwise, it's a blank line or comment, so do nothing + : + fi + # Increment the iterator +1 + i=$((i+1)) + # but if the limit of lines we want to see is exceeded + if [[ -z ${limit} ]]; then + # do nothing + : + elif [[ $i -eq ${limit} ]]; then + break + fi + done < "${filename}" + # Now the we have made an array of the file's content + for each_line in "${file_content[@]}"; do + # Print each line + # At some point, we may want to check the file line-by-line, so that's the reason for an array + log_write " ${each_line}" + done + fi +} + +parse_file() { + # Set the first argument passed to this function as a named variable for better readability + local filename="${1}" + # 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' command eval 'file_info=( $(cat "${filename}") )' + + # Set a named variable for better readability + local file_lines + # For each line in the file, + for file_lines in "${file_info[@]}"; do + if [[ ! -z "${file_lines}" ]]; then + # don't include the Web password hash + [[ "${file_linesline}" =~ ^\#.*$ || ! "${file_lines}" || "${file_lines}" == "WEBPASSWORD="* ]] && continue + # otherwise, display the lines of the file + log_write " ${file_lines}" + fi + done + # Set the IFS back to what it was + IFS="$OLD_IFS" +} + +check_name_resolution() { + # Check name resoltion from localhost, Pi-hole's IP, and Google's name severs + # 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 +} + +# 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 + local directory="${1}" + # Display the current test that is running + echo_current_diagnostic "contents of ${COL_CYAN}${directory}${COL_NC}" + # For each file in the directory, + for filename in ${directory}; do + # check if exists first; if it does, + if ls "${filename}" 1> /dev/null 2>&1; then + # do nothing + : + else + # Otherwise, show an error + log_write "${COL_RED}${directory} does not exist.${COL_NC}" + fi + done +} + +list_files_in_dir() { + # Set the first argument passed to tihs function as a named variable for better readability + local dir_to_parse="${1}" + # Store the files found in an array + local 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 + : + elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_BLOCKLIST_FILE}" ]] || \ + [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \ + [[ ${dir_to_parse}/${each_file} == ${PIHOLE_RAW_BLOCKLIST_FILES} ]] || \ + [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \ + [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_SETUP_VARS_FILE}" ]] || \ + [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG}" ]] || \ + [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_WEB_SERVER_ACCESS_LOG_FILE}" ]] || \ + [[ ${dir_to_parse}/${each_file} == ${PIHOLE_LOG_GZIPS} ]]; then + : + 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}" + # 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 + ;; + # Same for the FTL log + "${PIHOLE_FTL_LOG}") make_array_from_file "${dir_to_parse}/${each_file}" 25 + ;; + # parse the file into an array in case we ever need to analyze it line-by-line + *) make_array_from_file "${dir_to_parse}/${each_file}"; + esac + else + # Otherwise, do nothing since it's not a file needed for Pi-hole so we don't care about it + : + fi + done + fi + done +} + +show_content_of_files_in_dir() { + # Set a local variable for better readability + local directory="${1}" + # Check if the directory exists + dir_check "${directory}" + # if it does, list the files in it + list_files_in_dir "${directory}" +} + +show_content_of_pihole_files() { + # Show the content of the files in each of Pi-hole's folders + show_content_of_files_in_dir "${PIHOLE_DIRECTORY}" + show_content_of_files_in_dir "${DNSMASQ_D_DIRECTORY}" + show_content_of_files_in_dir "${WEB_SERVER_CONFIG_DIRECTORY}" + show_content_of_files_in_dir "${CRON_D_DIRECTORY}" + show_content_of_files_in_dir "${WEB_SERVER_LOG_DIRECTORY}" + show_content_of_files_in_dir "${LOG_DIRECTORY}" +} + +analyze_gravity_list() { + echo_current_diagnostic "Gravity list" + local head_line + local tail_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 gravity_permissions=$(ls -ld "${PIHOLE_BLOCKLIST_FILE}") + log_write "${COL_GREEN}${gravity_permissions}${COL_NC}" + local gravity_head=() + gravity_head=( $(head -n 4 ${PIHOLE_BLOCKLIST_FILE}) ) + log_write " ${COL_CYAN}-----head of $(basename ${PIHOLE_BLOCKLIST_FILE})------${COL_NC}" + for head_line in "${gravity_head[@]}"; do + log_write " ${head_line}" + done + log_write "" + local gravity_tail=() + gravity_tail=( $(tail -n 4 ${PIHOLE_BLOCKLIST_FILE}) ) + log_write " ${COL_CYAN}-----tail of $(basename ${PIHOLE_BLOCKLIST_FILE})------${COL_NC}" + for tail_line in "${gravity_tail[@]}"; do + log_write " ${tail_line}" + done + # Set the IFS back to what it was + 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=$(ls -ld "${PIHOLE_LOG}") + log_write "${COL_GREEN}${pihole_log_permissions}${COL_NC}" + local pihole_log_head=() + 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 + 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 + fi + fi + done + log_write "" + # Set the IFS back to what it was + IFS="$OLD_IFS" +} + +tricorder_use_nc_or_ssl() { + # Users can submit their debug logs using nc (unencrypted) or openssl (enrypted) if available + # Check for openssl first since encryption is a good thing + if command -v openssl &> /dev/null; then + # If the command exists, + log_write " * Using ${COL_GREEN}openssl${COL_NC} for transmission." + # encrypt and transmit the log and store the token returned in a variable + tricorder_token=$(< ${PIHOLE_DEBUG_LOG_SANITIZED} openssl s_client -quiet -connect tricorder.pi-hole.net:${TRICORDER_SSL_PORT_NUMBER} 2> /dev/null) + # 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_SANITIZED} nc tricorder.pi-hole.net ${TRICORDER_NC_PORT_NUMBER}) + fi +} + + +upload_to_tricorder() { + local username="pihole" + # Set the permissions and owner + chmod 644 ${PIHOLE_DEBUG_LOG} + chown "$USER":"${username}" ${PIHOLE_DEBUG_LOG} + + # Let the user know debugging is complete with something strikingly visual + log_write "" + log_write "${COL_PURPLE}********************************************${COL_NC}" + log_write "${COL_PURPLE}********************************************${COL_NC}" + 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) if [[ "${AUTOMATED}" ]]; then - echo "::: Debug script running in automated mode, uploading log to tricorder..." - tricorder=$(cat /var/log/pihole_debug.log | nc tricorder.pi-hole.net 9999) + # 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_ssl + # If we're not running in automated mode, else - read -r -p "::: Would you like to upload the log? [y/N] " response + 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 - [yY][eE][sS]|[yY]) - tricorder=$(cat /var/log/pihole_debug.log | nc tricorder.pi-hole.net 9999) - ;; - *) - echo "::: Log will NOT be uploaded to tricorder." - ;; + # If they say yes, run our function for uploading the log + [yY][eE][sS]|[yY]) tricorder_use_nc_or_ssl;; + # 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 fi - # Check if tricorder.pi-hole.net is reachable and provide token. - if [ -n "${tricorder}" ]; then - echo "::: ---=== Your debug token is : ${tricorder} Please make a note of it. ===---" - echo "::: Contact the Pi-hole team with your token for assistance." - echo "::: Thank you." - else - echo "::: There was an error uploading your debug log." - echo "::: Please try again or contact the Pi-hole team for assistance." + # Check if tricorder.pi-hole.net is reachable and provide token + # along with some additional useful information + if [[ -n "${tricorder_token}" ]]; then + # 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 "${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 "" + 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}." + # 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." fi - echo "::: A local copy of the Debug log can be found at : /var/log/pihole_debug.log" + # 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_SANITIZED}${COL_NC}\n" } -### END FUNCTIONS ### -# Create temporary file for log -TEMPLOG=$(mktemp /tmp/pihole_temp.XXXXXX) -# Open handle 3 for templog -exec 3>"$TEMPLOG" -# Delete templog, but allow for addressing via file handle. -rm "$TEMPLOG" - -# Create temporary file for logdump using file handle 4 -DUMPLOG=$(mktemp /tmp/pihole_temp.XXXXXX) -exec 4>"$DUMPLOG" -rm "$DUMPLOG" - -# Gather version of required packages / repositories -version_check || echo "REQUIRED FILES MISSING" -# Check for newer setupVars storage file -source_file "/etc/pihole/setupVars.conf" -# Gather information about the running distribution -distro_check || echo "Distro Check soft fail" -# Gather processor type -processor_check || echo "Processor Check soft fail" - -ip_check 6 ${IPV6_ADDRESS} -ip_check 4 ${IPV4_ADDRESS} - -daemon_check lighttpd http -daemon_check dnsmasq domain -daemon_check pihole-FTL 4711 -checkProcesses - -# Check local/IP/Google for IPv4 Resolution -testResolver 4 "${IPV4_ADDRESS%/*}" -# If IPv6 enabled, check resolution -if [[ "${IPV6_ADDRESS}" ]]; then - testResolver 6 "${IPV6_ADDRESS%/*}" -fi -# Poll dnsmasq Pi-hole specific queries -testChaos - -debugLighttpd - -files_check "${DNSMASQFILE}" -dir_check "${DNSMASQCONFDIR}" -files_check "${WHITELISTFILE}" -files_check "${BLACKLISTFILE}" -files_check "${ADLISTFILE}" - - -header_write "Analyzing gravity.list" - - gravity_length=$(grep -c ^ "${GRAVITYFILE}") \ - && log_write "${GRAVITYFILE} is ${gravity_length} lines long." \ - || log_echo "Warning: No gravity.list file found!" - -header_write "Analyzing pihole.log" - - pihole_length=$(grep -c ^ "${PIHOLELOG}") \ - && log_write "${PIHOLELOG} is ${pihole_length} lines long." \ - || log_echo "Warning: No pihole.log file found!" - - pihole_size=$(du -h "${PIHOLELOG}" | awk '{ print $1 }') \ - && log_write "${PIHOLELOG} is ${pihole_size}." \ - || log_echo "Warning: No pihole.log file found!" - -header_write "Analyzing pihole-FTL.log" - - FTL_length=$(grep -c ^ "${FTLLOG}") \ - && log_write "${FTLLOG} is ${FTL_length} lines long." \ - || log_echo "Warning: No pihole-FTL.log file found!" - - FTL_size=$(du -h "${FTLLOG}" | awk '{ print $1 }') \ - && log_write "${FTLLOG} is ${FTL_size}." \ - || log_echo "Warning: No pihole-FTL.log file found!" - -tail -n50 "${FTLLOG}" >&3 - -trap finalWork EXIT - -### Method calls for additional logging ### -dumpPiHoleLog +# Run through all the functions we made +make_temporary_log +initiate_debug +# setupVars.conf needs to be sourced before the networking so the values are +# available to the other functions +source_setup_variables +check_component_versions +check_critical_program_versions +diagnose_operating_system +check_selinux +processor_check +check_networking +check_name_resolution +process_status +parse_setup_vars +check_x_headers +analyze_gravity_list +show_content_of_pihole_files +analyze_pihole_log +copy_to_debug_log +upload_to_tricorder diff --git a/advanced/Scripts/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index cc553b32..2187f3ac 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -8,8 +8,11 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. +colfile="/opt/pihole/COL_TABLE" +source ${colfile} + if [[ "$@" != *"quiet"* ]]; then - echo -n "::: Flushing /var/log/pihole.log ..." + echo -ne " ${INFO} Flushing /var/log/pihole.log ..." fi if [[ "$@" == *"once"* ]]; then # Nightly logrotation @@ -41,5 +44,5 @@ else fi if [[ "$@" != *"quiet"* ]]; then - echo "... done!" + echo -e "${OVER} ${TICK} Flushed /var/log/pihole.log" fi diff --git a/advanced/Scripts/update.sh b/advanced/Scripts/update.sh index 4281d69f..a4ada4c8 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -10,10 +10,7 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. - - # Variables - readonly ADMIN_INTERFACE_GIT_URL="https://github.com/pi-hole/AdminLTE.git" readonly ADMIN_INTERFACE_DIR="/var/www/html/admin" readonly PI_HOLE_GIT_URL="https://github.com/pi-hole/pi-hole.git" @@ -22,9 +19,10 @@ readonly PI_HOLE_FILES_DIR="/etc/.pihole" # shellcheck disable=SC2034 PH_TEST=true -# Have to ignore the following rule as spaces in paths are not supported by ShellCheck -#shellcheck disable=SC1090 +# shellcheck disable=SC1090 source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" +# shellcheck disable=SC1091 +source "/opt/pihole/COL_TABLE" # is_repo() sourced from basic-install.sh # make_repo() sourced from basic-install.sh @@ -52,15 +50,15 @@ GitCheckUpdateAvail() { # defaults to the current one. REMOTE="$(git rev-parse "@{upstream}")" - if [[ ${#LOCAL} == 0 ]]; then - echo "::: Error: Local revision could not be obtained, ask Pi-hole support." - echo "::: Additional debugging output:" + if [[ "${#LOCAL}" == 0 ]]; then + echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support + Additional debugging output:${COL_NC}" git status exit fi - if [[ ${#REMOTE} == 0 ]]; then - echo "::: Error: Remote revision could not be obtained, ask Pi-hole support." - echo "::: Additional debugging output:" + if [[ "${#REMOTE}" == 0 ]]; then + echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support + Additional debugging output:${COL_NC}" git status exit fi @@ -80,7 +78,6 @@ GitCheckUpdateAvail() { } FTLcheckUpdate() { - local FTLversion FTLversion=$(/usr/bin/pihole-FTL tag) local FTLlatesttag @@ -96,57 +93,59 @@ FTLcheckUpdate() { main() { local pihole_version_current local web_version_current - #shellcheck disable=1090,2154 + local basicError="\\n ${COL_LIGHT_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}" + + # shellcheck disable=1090,2154 source "${setupVars}" - #This is unlikely + # This is unlikely if ! is_repo "${PI_HOLE_FILES_DIR}" ; then - echo "::: Critical Error: Core Pi-hole repo is missing from system!" - echo "::: Please re-run install script from https://github.com/pi-hole/pi-hole" + echo -e "\\n ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system! + Please re-run install script from https://pi-hole.net${COL_NC}" exit 1; fi - echo "::: Checking for updates..." + echo -e " ${INFO} Checking for updates..." if GitCheckUpdateAvail "${PI_HOLE_FILES_DIR}" ; then core_update=true - echo "::: Pi-hole Core: update available" + echo -e " ${INFO} Pi-hole Core:\\t${COL_YELLOW}update available${COL_NC}" else core_update=false - echo "::: Pi-hole Core: up to date" + echo -e " ${INFO} Pi-hole Core:\\t${COL_LIGHT_GREEN}up to date${COL_NC}" fi if FTLcheckUpdate ; then FTL_update=true - echo "::: FTL: update available" + echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}" else FTL_update=false - echo "::: FTL: up to date" + echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_GREEN}up to date${COL_NC}" fi # Logic: Don't update FTL when there is a core update available # since the core update will run the installer which will itself # re-install (i.e. update) FTL if ${FTL_update} && ! ${core_update}; then - echo ":::" - echo "::: FTL out of date" + echo "" + echo -e " ${INFO} FTL out of date" FTLdetect - echo ":::" + echo "" fi - if [[ ${INSTALL_WEB} == true ]]; then + if [[ "${INSTALL_WEB}" == true ]]; then if ! is_repo "${ADMIN_INTERFACE_DIR}" ; then - echo "::: Critical Error: Web Admin repo is missing from system!" - echo "::: Please re-run install script from https://github.com/pi-hole/pi-hole" + echo -e "\\n ${COL_LIGHT_RED}Error: Web Admin repo is missing from system! + Please re-run install script from https://pi-hole.net${COL_NC}" exit 1; fi if GitCheckUpdateAvail "${ADMIN_INTERFACE_DIR}" ; then web_update=true - echo "::: Web Interface: update available" + echo -e " ${INFO} Web Interface:\\t${COL_YELLOW}update available${COL_NC}" else web_update=false - echo "::: Web Interface: up to date" + echo -e " ${INFO} Web Interface:\\t${COL_LIGHT_GREEN}up to date${COL_NC}" fi # Logic @@ -161,72 +160,69 @@ main() { if ! ${core_update} && ! ${web_update} ; then if ! ${FTL_update} ; then - echo ":::" - echo "::: Everything is up to date!" + echo "" + echo -e " ${TICK} Everything is up to date!" exit 0 fi - elif ! ${core_update} && ${web_update} ; then - echo ":::" - echo "::: Pi-hole Web Admin files out of date" + echo "" + echo -e " ${INFO} Pi-hole Web Admin files out of date" getGitFiles "${ADMIN_INTERFACE_DIR}" "${ADMIN_INTERFACE_GIT_URL}" - elif ${core_update} && ! ${web_update} ; then - echo ":::" - echo "::: Pi-hole core files out of date" + echo "" + echo -e " ${INFO} Pi-hole core files out of date" getGitFiles "${PI_HOLE_FILES_DIR}" "${PI_HOLE_GIT_URL}" - ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --unattended || echo "Unable to complete update, contact Pi-hole" && exit 1 - + ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --unattended || \ + echo -e "${basicError}" && exit 1 elif ${core_update} && ${web_update} ; then - echo ":::" - echo "::: Updating Pi-hole core and web admin files" + echo "" + echo -e " ${INFO} Updating Pi-hole core and web admin files" getGitFiles "${PI_HOLE_FILES_DIR}" "${PI_HOLE_GIT_URL}" - ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --unattended || echo "Unable to complete update, contact Pi-hole" && exit 1 + ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --unattended || \ + echo -e "${basicError}" && exit 1 else - echo "*** Update script has malfunctioned, fallthrough reached. Please contact support" + echo -e " ${COL_LIGHT_RED}Update script has malfunctioned, please contact Pi-hole Support${COL_NC}" exit 1 fi else # Web Admin not installed, so only verify if core is up to date if ! ${core_update}; then if ! ${FTL_update} ; then - echo ":::" - echo "::: Everything is up to date!" + echo "" + echo -e " ${INFO} Everything is up to date!" exit 0 fi else - echo ":::" - echo "::: Pi-hole core files out of date" + echo "" + echo -e " ${INFO} Pi-hole Core files out of date" getGitFiles "${PI_HOLE_FILES_DIR}" "${PI_HOLE_GIT_URL}" - ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --unattended || echo "Unable to complete update, contact Pi-hole" && exit 1 + ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --unattended || \ + echo -e "${basicError}" && exit 1 fi fi if [[ "${web_update}" == true ]]; then web_version_current="$(/usr/local/bin/pihole version --admin --current)" - echo ":::" - echo "::: Web Admin version is now at ${web_version_current/* v/v}}" - echo "::: If you had made any changes in '/var/www/html/admin/', they have been stashed using 'git stash'" + echo "" + echo -e " ${INFO} Web Admin version is now at ${web_version_current/* v/v} + ${INFO} If you had made any changes in '/var/www/html/admin/', they have been stashed using 'git stash'" fi if [[ "${core_update}" == true ]]; then pihole_version_current="$(/usr/local/bin/pihole version --pihole --current)" - echo ":::" - echo "::: Pi-hole version is now at ${pihole_version_current/* v/v}}" - echo "::: If you had made any changes in '/etc/.pihole/', they have been stashed using 'git stash'" + echo "" + echo -e " ${INFO} Pi-hole version is now at ${pihole_version_current/* v/v} + ${INFO} If you had made any changes in '/etc/.pihole/', they have been stashed using 'git stash'" fi - if [[ ${FTL_update} == true ]]; then - FTL_version_current="$(/usr/local/bin/pihole version --ftl --current)" - echo ":::" - echo "::: FTL version is now at ${FTL_version_current/* v/v}}" + if [[ "${FTL_update}" == true ]]; then + FTL_version_current="$(/usr/bin/pihole-FTL tag)" + echo -e "\\n ${INFO} FTL version is now at ${FTL_version_current/* v/v}" start_service pihole-FTL enable_service pihole-FTL fi - echo "" exit 0 - } main diff --git a/advanced/Scripts/updatecheck.sh b/advanced/Scripts/updatecheck.sh new file mode 100755 index 00000000..9b79c4cb --- /dev/null +++ b/advanced/Scripts/updatecheck.sh @@ -0,0 +1,59 @@ +#!/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. +# +# Checks for updates via GitHub +# +# 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 +} + +GITHUB_CORE_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/pi-hole/releases/latest' 2> /dev/null)")" +GITHUB_WEB_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/AdminLTE/releases/latest' 2> /dev/null)")" +GITHUB_FTL_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/FTL/releases/latest' 2> /dev/null)")" + +echo "${GITHUB_CORE_VERSION} ${GITHUB_WEB_VERSION} ${GITHUB_FTL_VERSION}" > "/etc/pihole/GitHubVersions" + +function get_local_branch() { + # Return active branch + cd "${1}" 2> /dev/null || return 1 + git rev-parse --abbrev-ref HEAD || return 1 +} + +CORE_BRANCH="$(get_local_branch /etc/.pihole)" +WEB_BRANCH="$(get_local_branch /var/www/html/admin)" +#FTL_BRANCH="$(pihole-FTL branch)" +# Don't store FTL branch until the next release of FTL which +# supports returning the branch in an easy way +FTL_BRANCH="XXX" + +echo "${CORE_BRANCH} ${WEB_BRANCH} ${FTL_BRANCH}" > "/etc/pihole/localbranches" + +function get_local_version() { + # Return active branch + cd "${1}" 2> /dev/null || return 1 + git describe --long --dirty --tags || return 1 +} + +CORE_VERSION="$(get_local_version /etc/.pihole)" +WEB_VERSION="$(get_local_version /var/www/html/admin)" +FTL_VERSION="$(pihole-FTL version)" + +echo "${CORE_VERSION} ${WEB_VERSION} ${FTL_VERSION}" > "/etc/pihole/localversions" diff --git a/advanced/Scripts/webpage.sh b/advanced/Scripts/webpage.sh index 8419aa8d..07bc160f 100755 --- a/advanced/Scripts/webpage.sh +++ b/advanced/Scripts/webpage.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. @@ -14,20 +16,26 @@ readonly dhcpconfig="/etc/dnsmasq.d/02-pihole-dhcp.conf" # 03 -> wildcards readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf" +coltable="/opt/pihole/COL_TABLE" +if [[ -f ${coltable} ]]; then + source ${coltable} +fi + helpFunc() { echo "Usage: pihole -a [options] Example: pihole -a -p password Set options for the Admin Console Options: - -f, flush Flush the Pi-hole log -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 - Add '-h' for more info on interface usage" + Add '-h' for more info on interface usage" exit 0 } @@ -58,6 +66,7 @@ delete_dnsmasq_setting() { SetTemperatureUnit() { change_setting "TEMPERATUREUNIT" "${unit}" + echo -e " ${TICK} Set temperature unit to ${unit}" } HashPassword() { @@ -84,12 +93,15 @@ SetWebPassword() { readonly PASSWORD="${args[2]}" readonly CONFIRM="${PASSWORD}" else + # Prevents a bug if the user presses Ctrl+C and it continues to hide the text typed. + # So we reset the terminal via stty if the user does press Ctrl+C + trap '{ echo -e "\nNo password will be set" ; stty sane ; exit 1; }' INT read -s -p "Enter New Password (Blank for no password): " PASSWORD echo "" if [ "${PASSWORD}" == "" ]; then change_setting "WEBPASSWORD" "" - echo "Password Removed" + echo -e " ${TICK} Password Removed" exit 0 fi @@ -98,12 +110,12 @@ SetWebPassword() { fi if [ "${PASSWORD}" == "${CONFIRM}" ] ; then - hash=$(HashPassword ${PASSWORD}) + hash=$(HashPassword "${PASSWORD}") # Save hash to file change_setting "WEBPASSWORD" "${hash}" - echo "New password set" + echo -e " ${TICK} New password set" else - echo "Passwords don't match. Your password has not been changed" + echo -e " ${CROSS} Passwords don't match. Your password has not been changed" exit 1 fi } @@ -208,16 +220,16 @@ SetExcludeClients() { change_setting "API_EXCLUDE_CLIENTS" "${args[2]}" } +Poweroff(){ + nohup bash -c "sleep 5; poweroff" &> /dev/null /dev/null /dev/null - else - service dnsmasq restart &> /dev/null - fi + /usr/local/bin/pihole restartdns } SetQueryLogOptions() { @@ -243,7 +255,12 @@ ProcessDHCPSettings() { if [[ "${DHCP_LEASETIME}" == "0" ]]; then leasetime="infinite" elif [[ "${DHCP_LEASETIME}" == "" ]]; then - leasetime="24h" + 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" @@ -275,7 +292,9 @@ ra-param=*,0,0 fi else - rm "${dhcpconfig}" &> /dev/null + if [[ -f "${dhcpconfig}" ]]; then + rm "${dhcpconfig}" &> /dev/null + fi fi } @@ -373,12 +392,23 @@ RemoveDHCPStaticAddress() { } SetHostRecord() { - if [ -n "${args[3]}" ]; then + 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 "Setting host record for ${args[2]} -> ${args[3]}" + echo -e " ${TICK} Setting host record for ${args[2]} to ${args[3]}" else change_setting "HOSTRECORD" "" - echo "Removing host record" + echo -e " ${TICK} Removing host record" fi ProcessDNSSettings @@ -387,9 +417,30 @@ SetHostRecord() { 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 + 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() { source "${setupVars}" - + if [[ "$3" == "-h" ]] || [[ "$3" == "--help" ]]; then echo "Usage: pihole -a -i [interface] Example: 'pihole -a -i local' @@ -402,15 +453,15 @@ Interfaces: all Listen on all interfaces, permit all origins" exit 0 fi - + if [[ "${args[2]}" == "all" ]]; then - echo "Listening on all interfaces, permiting all origins, hope you have a firewall!" + echo -e " ${INFO} Listening on all interfaces, permiting all origins. Please use a firewall!" change_setting "DNSMASQ_LISTENING" "all" elif [[ "${args[2]}" == "local" ]]; then - echo "Listening on all interfaces, permitting only origins that are at most one hop away (local devices)" + echo -e " ${INFO} Listening on all interfaces, permiting origins from one hop away (LAN)" change_setting "DNSMASQ_LISTENING" "local" else - echo "Listening only on interface ${PIHOLE_INTERFACE}" + echo -e " ${INFO} Listening only on interface ${PIHOLE_INTERFACE}" change_setting "DNSMASQ_LISTENING" "single" fi @@ -428,6 +479,11 @@ Teleporter() { php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "pi-hole-teleporter_${datetimestamp}.zip" } +audit() +{ + echo "${args[2]}" >> /etc/pihole/auditlog.list +} + main() { args=("$@") @@ -439,6 +495,7 @@ main() { "setdns" ) SetDNSServers;; "setexcludedomains" ) SetExcludeDomains;; "setexcludeclients" ) SetExcludeClients;; + "poweroff" ) Poweroff;; "reboot" ) Reboot;; "restartdns" ) RestartDNS;; "setquerylog" ) SetQueryLogOptions;; @@ -450,10 +507,12 @@ main() { "resolve" ) ResolutionSettings;; "addstaticdhcp" ) AddDHCPStaticAddress;; "removestaticdhcp" ) RemoveDHCPStaticAddress;; - "hostrecord" ) SetHostRecord;; + "-r" | "hostrecord" ) SetHostRecord "$3";; + "-e" | "email" ) SetAdminEmail "$3";; "-i" | "interface" ) SetListeningMode "$@";; "-t" | "teleporter" ) Teleporter;; "adlist" ) CustomizeAdLists;; + "audit" ) audit;; * ) helpFunc;; esac diff --git a/advanced/blockingpage.css b/advanced/blockingpage.css index 7e11dbd0..e74844d1 100644 --- a/advanced/blockingpage.css +++ b/advanced/blockingpage.css @@ -1,136 +1,383 @@ -/* CSS Reset */ -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } -article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } -body { line-height: 1; } -ol, ul { list-style: none; } -blockquote, q { quotes: none; } -blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } -table { border-collapse: collapse; border-spacing: 0; } -html { height: 100%; overflow-x: hidden; } +/* 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. */ -/* General Style */ -a { color: rgba(0,60,120,0.95); text-decoration: none; } /* 1E3C5A */ -a:hover { color: rgba(210,120,0,0.95); transition-duration: .2s; } /* 255, 128, 0 */ -divs a { border-bottom: 1px dashed rgba(30,60,90,0.3); } -b { font-weight: bold; } -i { font-style: italic; } +/* 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"; } -footer, pre, td { font-family: monospace; padding-left: 15px; } -/*body, header { background: #E1E1E1; }*/ +#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-image: -webkit-linear-gradient(top, rgba(240,240,240,0.95), rgba(190,190,190,0.95)); - background-image: linear-gradient(to bottom, rgba(240,240,240,0.95), rgba(190,190,190,0.95)); - background-attachment: fixed; - color: rgba(64,64,64,0.95); - font: 14px, sans-serif; - line-height: 1em; + 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 { - min-width: 320px; - width: 100%; - text-shadow: 0 1px rgba(255,255,255,0.6); - display: table; - table-layout: fixed; - border: 1px solid rgba(0,0,0,0.25); - border-top-color: rgba(255,255,255,0.85); - border-style: solid none; - background-image: -webkit-linear-gradient(top, rgba(240,240,240,0.95), rgba(220,220,220,0.95)); - background-image: linear-gradient(to bottom, rgba(240,240,240,0.95), rgba(220,220,220,0.95)); - box-shadow: 0 0 1px 1px rgba(0,0,0,0.04); + background: #3c8dbc; + display: table; + position: relative; + width: 100%; } -header h1, header div { - display: table-cell; - color: inherit; - font-weight: bold; - vertical-align: middle; - white-space: nowrap; - overflow: hidden; - box-sizing: border-box; +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 */ } -header h1 { - font-size: 22px; - font-weight: bold; - width: 100%; - padding: 8px 0; - text-indent: 32px; - background: url("http://pi.hole/admin/img/logo.svg") left no-repeat; - background-size: 30px 22px; +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; } -header h1 a, h1 a:hover { color: inherit; } -header .alt { width: 85px; font-size: 0.8em; padding-right: 4px; text-align: right; line-height: 1.25em; } -.active { color: green; } -.inactive { color: red; } +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 { - display: block; - width: 80%; - padding: 10px; - font-size: 1em; - background-color: rgba(255,255,255,0.85); - margin: 8px auto; - box-sizing: border-box; - border: 1px solid rgba(0,0,0,0.25); - box-shadow: 4px 4px rgba(0,0,0,0.1); - line-height: 1.2em; - border-radius: 8px; + background: #ecf0f5; + font-size: 1.65rem; + padding: 10px; } -h2 { /* Rgba is shared with .transparent th */ - font: 1.15em sans-serif; - background-color: rgba(255,0,0,0.4); - text-shadow: none; - line-height: 1.1em; - padding-bottom: 1px; - margin-top: 8px; - margin-bottom: 4px; - background: -webkit-linear-gradient(left, rgba(0,0,0,0.25), transparent 80%) no-repeat; - background: linear-gradient(to right, rgba(0,0,0,0.25), transparent 80%) no-repeat; - background-size: 100% 1px; - background-position: 0 17px; +#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; } -h2:first-child { margin-top: 0; } -h2 ~ *:not(h2) { margin-left: 4px; } -li { padding: 2px 0; } -li::before { content: "\00BB\00a0"; } -li a { position: relative; top: 1px; } /* Center bullet-point arrows */ - -/* Button Style */ -.buttons a, button, input, .transparent th a { /* Swapped rgba is shared with input[type='url'] */ - display: inline-block; - color: rgba(32,32,32,0.9); - font-weight: bold; - text-align: center; - cursor: pointer; - text-shadow: 0 1px rgba(255,255,255,0.2); - line-height: 0.86em; - font-size: 1em; - padding: 4px 8px; - background: #FAFAFA; - background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.05), rgba(0,0,0,0.05)); - background-image: linear-gradient(to bottom, rgba(255,255,255,0.05), rgba(0,0,0,0.05)); - border: 1px solid rgba(0,0,0,0.25); - border-radius: 4px; - box-shadow: 0 1px 0 rgba(0,0,0,0.04); +#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; } -.buttons { white-space: nowrap; width: 100%; display: table; } -.buttons33 { white-space: nowrap; width: 33.333%; display: table; text-align: center; margin-left: 33.333% } -.mini a { width: 50%; } -a.safe { background-color: rgba(0,220,0,0.5); } -button.safe { background-color: rgba(0,220,0,0.5); } -a.warn { background-color: rgba(220,0,0,0.5); } +#bpOutput.hidden { display: none; } +#bpOutput.success { background: #00a65a; } +#bpOutput.error { background: #dd4b39; } -.blocked a, .mini a { display: table-cell; } -.blocked a.safe50 { width: 50%; background-color: rgba(0,220,0,0.5); } -.blocked a.safe33 { width: 33.333%; background-color: rgba(0,220,0,0.5); } +.blockMsg, .flagMsg { + font: bold 1.8rem Consolas, Courier, monospace; + padding: 5px 10px 10px 10px; + text-indent: 15px; +} -/* Types of text */ -.msg { white-space: pre; overflow: auto; -webkit-overflow-scrolling: touch; display: block; line-height: 1.2em; font-weight: bold; font-size: 1.15em; margin: 4px 8px 8px 8px; white-space: pre-line; } +#bpHelpTxt { padding-bottom: 10px; } -footer { font-size: 0.8em; text-align: center; width: 87%; margin: 4px auto; } +.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/index.js b/advanced/index.js deleted file mode 100644 index c9da5aff..00000000 --- a/advanced/index.js +++ /dev/null @@ -1 +0,0 @@ -var x = "Pi-hole: A black hole for Internet advertisements." diff --git a/advanced/index.php b/advanced/index.php index 1dd5acc7..5c2f250d 100644 --- a/advanced/index.php +++ b/advanced/index.php @@ -1,224 +1,345 @@ /etc/pihole/setupVars.conf"); -// If the server name is 'pi.hole', it's likely a user trying to get to the admin panel. -// Let's be nice and redirect them. -if ($serverName === 'pi.hole') -{ - header('HTTP/1.1 301 Moved Permanently'); - header("Location: /admin/"); -} - -// Retrieve server URI extension (EG: jpg, exe, php) -ini_set('pcre.recursion_limit',100); -$uriExt = pathinfo($uri, PATHINFO_EXTENSION); - -// Define which URL extensions get rendered as "Website Blocked" -$webExt = array('asp', 'htm', 'html', 'php', 'rss', 'xml'); - -// Get IPv4 and IPv6 addresses from setupVars.conf (if available) +// Get values from setupVars.conf $setupVars = parse_ini_file("/etc/pihole/setupVars.conf"); -$ipv4 = isset($setupVars["IPV4_ADDRESS"]) ? explode("/", $setupVars["IPV4_ADDRESS"])[0] : $_SERVER['SERVER_ADDR']; -$ipv6 = isset($setupVars["IPV6_ADDRESS"]) ? explode("/", $setupVars["IPV6_ADDRESS"])[0] : $_SERVER['SERVER_ADDR']; +$svPasswd = !empty($setupVars["WEBPASSWORD"]); +$svEmail = (!empty($setupVars["ADMIN_EMAIL"]) && filter_var($setupVars["ADMIN_EMAIL"], FILTER_VALIDATE_EMAIL)) ? $setupVars["ADMIN_EMAIL"] : ""; +unset($setupVars); -$AUTHORIZED_HOSTNAMES = array( - $ipv4, - $ipv6, - str_replace(array("[","]"), array("",""), $_SERVER["SERVER_ADDR"]), - "pi.hole", - "localhost"); -// Allow user set virtual hostnames -$virtual_host = getenv('VIRTUAL_HOST'); -if (!empty($virtual_host)) - array_push($AUTHORIZED_HOSTNAMES, $virtual_host); +// Set landing page location, found within /var/www/html/ +$landPage = "../landing.php"; -// Immediately quit since we didn't block this page (the IP address or pi.hole is explicitly requested) -if(validIP($serverName) || in_array($serverName,$AUTHORIZED_HOSTNAMES)) -{ - http_response_code(404); - die(); +// Define array for hostnames to be accepted as self address for splash page +$authorizedHosts = []; +if (!empty($_SERVER["FQDN"])) { + // If setenv.add-environment = ("fqdn" => "true") is configured in lighttpd, + // append $serverName to $authorizedHosts + array_push($authorizedHosts, $serverName); +} else if (!empty($_SERVER["VIRTUAL_HOST"])) { + // Append virtual hostname to $authorizedHosts + array_push($authorizedHosts, $_SERVER["VIRTUAL_HOST"]); } -if(in_array($uriExt, $webExt) || empty($uriExt)) -{ - // Requested resource has an extension listed in $webExt - // or no extension (index access to some folder incl. the root dir) - $showPage = true; -} -else -{ - // Something else - $showPage = false; +// 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); + +// Check if this is served over HTTP or HTTPS +if(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on") { + $proto = "https"; +} else { + $proto = "http"; } -// Handle incoming URI types -if (!$showPage) -{ -?> - - - - - - - -'; + +// 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"); } -// Get Pi-hole version -$piHoleVersion = exec('cd /etc/.pihole/ && git describe --tags --abbrev=0'); +// Determine block page type +if ($serverName === "pi.hole") { + // Redirect to Web Interface + exit(header("Location: /admin")); +} 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 + "; -// Don't show the URI if it is the root directory -if($uri == "/") -{ - $uri = ""; + // 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 */ + +// Determine placeholder text based off $svPasswd presence +$wlPlaceHolder = empty($svPasswd) ? "No admin password set" : "Javascript disabled"; + +// Define admin email address text based off $svEmail presence +$bpAskAdmin = !empty($svEmail) ? '' : ""; + +// Determine if at least one block list has been generated +if (empty(glob("/etc/pihole/list.0.*.domains"))) + 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."); + +// Set location of adlists file +if (is_file("/etc/pihole/adlists.list")) { + $adLists = "/etc/pihole/adlists.list"; +} elseif (is_file("/etc/pihole/adlists.default")) { + $adLists = "/etc/pihole/adlists.default"; +} else { + die("[ERROR] File not found: /etc/pihole/adlists.list"); +} + +// Get all URLs starting with "http" or "www" from adlists and re-index array numerically +$adlistsUrls = array_values(preg_grep("/(^http)|(^www)/i", file($adLists, FILE_IGNORE_NEW_LINES))); + +if (empty($adlistsUrls)) + die("[ERROR]: There are no adlist URL's found within $adLists"); + +// 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()); + } +} + +// 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 ?> + - - Website Blocked - - - - + + + + + + + + ● <?=$serverName ?> + + - +
-

Website Blocked

+

+ +

+
+ + +
+
+
+

Open Source Ad Blocker + Designed for Raspberry Pi +

+
+ +
+ +
+ +
+
-
Access to the following site has been blocked:
-
-
If you have an ongoing use for this website, please ask the owner of the Pi-hole in your network to have it whitelisted.
- - - - This page is blocked because it is explicitly contained within the following block list(s): -
- - +
+
+

+
+ +
+

+
+ +
+
+ + 0) echo ''; ?> +
+ +
+ +
 0) foreach ($queryResults as $num => $value) { echo "[$num]:$adlistsUrls[$num]\n"; } ?>
+ +
+ + +
+
-
Generated by Pi-hole
- - - +
. Pi-hole ()
+
+ - - + diff --git a/advanced/lighttpd.conf.debian b/advanced/lighttpd.conf.debian index 3b57756e..b5bece72 100644 --- a/advanced/lighttpd.conf.debian +++ b/advanced/lighttpd.conf.debian @@ -2,18 +2,16 @@ # (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. - - ############################################################################### # FILE AUTOMATICALLY OVERWRITTEN BY PI-HOLE INSTALL/UPDATE PROCEDURE. # # ANY CHANGES MADE TO THIS FILE AFTER INSTALL WILL BE LOST ON THE NEXT UPDATE # # # -# CHANGES SHOULD BE MADE IN A SEPERATE CONFIG FILE: # +# CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE: # # /etc/lighttpd/external.conf # ############################################################################### @@ -39,9 +37,8 @@ server.port = 80 accesslog.filename = "/var/log/lighttpd/access.log" accesslog.format = "%{%s}t|%V|%r|%s|%b" - index-file.names = ( "index.php", "index.html", "index.lighttpd.html" ) -url.access-deny = ( "~", ".inc" ) +url.access-deny = ( "~", ".inc", ".md", ".yml", ".ini" ) static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" ) compress.cache-dir = "/var/cache/lighttpd/compress/" @@ -50,32 +47,29 @@ compress.filetype = ( "application/javascript", "text/css", "text/html # 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" + +# Prevent Lighttpd from enabling Let's Encrypt SSL for every blocked domain +#include_shell "/usr/share/lighttpd/include-conf-enabled.pl" +include_shell "find /etc/lighttpd/conf-enabled -name '*.conf' -a ! -name 'letsencrypt.conf' -printf 'include \"%p\"\n' 2>/dev/null" # If the URL starts with /admin, it is the Web interface $HTTP["url"] =~ "^/admin/" { - # Create a response header for debugging using curl -I + # Create a response header for debugging using curl -I setenv.add-response-header = ( "X-Pi-hole" => "The Pi-hole Web interface is working!", "X-Frame-Options" => "DENY" ) -} -# Rewite js requests, must be out of $HTTP block due to bug #2526 -url.rewrite = ( "^(?!/admin/).*\.js$" => "pihole/index.js" ) - -# If the URL does not start with /admin, then it is a query for an ad domain -$HTTP["url"] =~ "^(?!/admin)/.*" { - # Create a response header for debugging using curl -I - setenv.add-response-header = ( "X-Pi-hole" => "A black hole for Internet advertisements." ) -} - -# Entering just "pi.hole" into a browser redirects to "pi.hole/admin/" -$HTTP["host"] == "pi.hole" { - $HTTP["url"] == "/" { - url.redirect = ( "" => "/admin/" ) + $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 = ("") +} + # Add user chosen options held in external file include_shell "cat external.conf 2>/dev/null" diff --git a/advanced/lighttpd.conf.fedora b/advanced/lighttpd.conf.fedora index fd856fbb..43d94d84 100644 --- a/advanced/lighttpd.conf.fedora +++ b/advanced/lighttpd.conf.fedora @@ -7,13 +7,11 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. - - ############################################################################### # FILE AUTOMATICALLY OVERWRITTEN BY PI-HOLE INSTALL/UPDATE PROCEDURE. # # ANY CHANGES MADE TO THIS FILE AFTER INSTALL WILL BE LOST ON THE NEXT UPDATE # # # -# CHANGES SHOULD BE MADE IN A SEPERATE CONFIG FILE: # +# CHANGES SHOULD BE MADE IN A SEPARATE CONFIG FILE: # # /etc/lighttpd/external.conf # ############################################################################### @@ -42,7 +40,7 @@ accesslog.format = "%{%s}t|%V|%r|%s|%b" index-file.names = ( "index.php", "index.html", "index.lighttpd.html" ) -url.access-deny = ( "~", ".inc" ) +url.access-deny = ( "~", ".inc", ".md", ".yml", ".ini" ) static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" ) compress.cache-dir = "/var/cache/lighttpd/compress/" @@ -74,25 +72,22 @@ fastcgi.server = ( ".php" => # If the URL starts with /admin, it is the Web interface $HTTP["url"] =~ "^/admin/" { - # Create a response header for debugging using curl -I - setenv.add-response-header = ( "X-Pi-hole" => "The Pi-hole Web interface is working!" ) -} + # Create a response header for debugging using curl -I + setenv.add-response-header = ( + "X-Pi-hole" => "The Pi-hole Web interface is working!", + "X-Frame-Options" => "DENY" + ) -# Rewite js requests, must be out of $HTTP block due to bug #2526 -url.rewrite = ( "^(?!/admin/).*\.js$" => "pihole/index.js" ) - -# If the URL does not start with /admin, then it is a query for an ad domain -$HTTP["url"] =~ "^(?!/admin)/.*" { - # Create a response header for debugging using curl -I - setenv.add-response-header = ( "X-Pi-hole" => "A black hole for Internet advertisements." ) -} - -# Entering just "pi.hole" into a browser redirects to "pi.hole/admin/" -$HTTP["host"] == "pi.hole" { - $HTTP["url"] == "/" { - url.redirect = ( "" => "/admin/" ) + $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 = ("") +} + # Add user chosen options held in external file include_shell "cat external.conf 2>/dev/null" diff --git a/advanced/pihole.cron b/advanced/pihole.cron index f1beb08c..2273358b 100644 --- a/advanced/pihole.cron +++ b/advanced/pihole.cron @@ -14,8 +14,8 @@ # is updated or re-installed. Please make any changes to the appropriate crontab # or other cron file snippets. -# Pi-hole: Update the ad sources once a week on Sunday at 01:59 -# Download any updates from the adlists +# Pi-hole: Update the ad sources once a week on Sunday at a random time in the +# early morning. Download any updates from the adlists 59 1 * * 7 root PATH="$PATH:/usr/local/bin/" pihole updateGravity # Pi-hole: Update Pi-hole! Uncomment to enable auto update @@ -28,3 +28,6 @@ 00 00 * * * root PATH="$PATH:/usr/local/bin/" pihole flush once quiet @reboot root /usr/sbin/logrotate /etc/pihole/logrotate + +# Pi-hole: Grab remote version and branch every 10 minutes +*/10 * * * * root PATH="$PATH:/usr/local/bin/" pihole updatechecker diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 2cf2c61d..79754872 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -1,43 +1,65 @@ #!/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. # -# Installs Pi-hole +# Installs and Updates Pi-hole # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. - - # pi-hole.net/donate # -# Install with this command (from your Pi): +# Install with this command (from your Linux machine): # # curl -L install.pi-hole.net | bash +# -e option instructs bash to immediately exit if any command [1] has a non-zero exit status +# We do not want users to end up with a partially working install, so we exit the script +# instead of continuing the installation with something broken set -e + ######## 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 propogate to all instances of the variable +# These variables should all be GLOBAL variables, written in CAPS +# 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 + +# We write to a temporary file before moving the log to the pihole folder tmpLog=/tmp/pihole-install.log instalLogLoc=/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 +# Pi-hole uses lighttpd as a Web server, and this is the config file for it +# shellcheck disable=SC2034 lighttpdConfig=/etc/lighttpd/lighttpd.conf +# This is a file used for the colorized output +coltable=/opt/pihole/COL_TABLE +# We store several other folders and webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git" webInterfaceDir="/var/www/html/admin" piholeGitUrl="https://github.com/pi-hole/pi-hole.git" PI_HOLE_LOCAL_REPO="/etc/.pihole" +# These are the names of piholes files, stored in an array PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage) +# This folder is where the Pi-hole scripts will be installed PI_HOLE_INSTALL_DIR="/opt/pihole" useUpdateVars=false +# 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 QUERY_LOGGING=true INSTALL_WEB=true -# Find the rows and columns will default to 80x24 is it can not be detected +# Find the rows and columns will default to 80x24 if it can not be detected screen_size=$(stty size 2>/dev/null || echo 24 80) rows=$(echo "${screen_size}" | awk '{print $1}') columns=$(echo "${screen_size}" | awk '{print $2}') @@ -50,19 +72,41 @@ r=$(( r < 20 ? 20 : r )) c=$(( c < 70 ? 70 : c )) ######## 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 +# If the color table file exists, +if [[ -f "${coltable}" ]]; then + # source it + source ${coltable} +# Othwerise, +else + # Set these values so the installer can still run in color + COL_NC='\e[0m' # No Color + COL_LIGHT_GREEN='\e[1;32m' + COL_LIGHT_RED='\e[1;31m' + TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]" + CROSS="[${COL_LIGHT_RED}✗${COL_NC}]" + INFO="[i]" + # shellcheck disable=SC2034 + DONE="${COL_LIGHT_GREEN} done!${COL_NC}" + OVER="\\r\\033[K" +fi + +# 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 " - .;;,. + echo -e " + ${COL_LIGHT_GREEN}.;;,. .ccccc:,. :cccclll:. ..,, :ccccclll. ;ooodc 'ccll:;ll .oooodc .;cll.;;looo:. - .. ','. + ${COL_LIGHT_RED}.. ','. .',,,,,,'. .',,,,,,,,,,. .',,,,,,,,,,,,.... @@ -75,70 +119,97 @@ show_ascii_berry() { ....',,,,,,,,,,,,. .',,,,,,,,,'. .',,,,,,'. - ..'''. + ..'''.${COL_NC} " } - # Compatibility distro_check() { +# If apt-get is installed, then we know it's part of the Debian family if command -v apt-get &> /dev/null; then - #Debian Family - ############################################# + # 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" - # ######################################### - # fixes for dependency differences - # Debian 7 doesn't have iproute2 use iproute + # Some distros vary slightly so these fixes for dependencies may apply + # 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 - # Prefer the php metapackage if it's there, fall back on the php5 packages + # We prefer the php metapackage if it's there if ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1; then phpVer="php" + # If not, else + # fall back on the php5 packages phpVer="php5" 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 successfuly 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) - PIHOLE_DEPS=(bc cron curl dnsmasq dnsutils iputils-ping lsof netcat sudo unzip wget) - PIHOLE_WEB_DEPS=(lighttpd ${phpVer}-common ${phpVer}-cgi) + # Pi-hole itself has several dependencies that also need to be installed + PIHOLE_DEPS=(bc cron curl dnsmasq dnsutils iputils-ping lsof netcat sudo unzip wget idn2) + # 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" + # The DNS server user DNSMASQ_USER="dnsmasq" + # A function to check... test_dpkg_lock() { - i=0 - while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do - sleep 0.5 - ((i=i+1)) - done - # Always return success, since we only return if there is no - # lock (anymore) - return 0 - } + # 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 command -v rpm &> /dev/null; then - # Fedora Family + # Then check if dnf or yum is the package manager if command -v dnf &> /dev/null; 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. + # 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 net-tools newt procps-ng) - PIHOLE_DEPS=(bc bind-utils cronie curl dnsmasq findutils nmap-ncat sudo unzip wget) - PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php php-common php-cli) + PIHOLE_DEPS=(bc bind-utils cronie curl dnsmasq findutils nmap-ncat sudo unzip wget libidn2 psmisc) + PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php php-common php-cli php-pdo) if ! grep -q 'Fedora' /etc/redhat-release; then INSTALLER_DEPS=("${INSTALLER_DEPS[@]}" "epel-release"); fi @@ -147,149 +218,233 @@ elif command -v rpm &> /dev/null; then LIGHTTPD_CFG="lighttpd.conf.fedora" DNSMASQ_USER="nobody" +# If neither apt-get or rmp/dnf are found else - echo "OS distribution not supported" + # it's not an OS we can support, + echo -e " ${CROSS} OS distribution not supported" + # so exit the installer exit fi } +# A function for checking if a folder is a git repository is_repo() { - # Use git to check if directory is currently under VCS, return the value 128 - # if directory is not a repo. Return 1 if directory does not exist. + # Use a named, local variable instead of the vague $1, which is the first arguement 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 - # git -C is not used here to support git versions older than 1.8.4 + # move into the directory cd "${directory}" + # Use git to check if the folder is a repo + # git -C is not used here to support git versions older than 1.8.4 git status --short &> /dev/null || rc=$? + # If the command was not successful, else - # non-zero return code if directory does not exist + # Set a non-zero return code if directory does not exist rc=1 fi + # Move back into the directory the user started in cd "${curdir}" + # Return the code; if one is not set, return 0 return "${rc:-0}" } +# A function to clone a repo make_repo() { + # Set named variables for better readability local directory="${1}" local remoteRepo="${2}" - - echo -n "::: Cloning ${remoteRepo} into ${directory}..." - # Clean out the directory if it exists for git to clone into + # 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 + echo -ne " ${INFO} ${str}..." + # If the directory exists, if [[ -d "${directory}" ]]; then + # delete everything in it so git can clone into it rm -rf "${directory}" fi + # Clone the repo and return the return code from this command git clone -q --depth 1 "${remoteRepo}" "${directory}" &> /dev/null || return $? - echo " done!" + # Show a colored message showing it's status + echo -e "${OVER} ${TICK} ${str}" + # Always return 0? Not sure this is correct return 0 } +# We need to make sure the repos are up-to-date so we can effectively install Clean out the directory if it exists for git to clone into update_repo() { + # Use named, local variables + # As you can see, these are the same variable names used in the last function, + # 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 + # 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 - # Pull the latest commits - echo -n "::: Updating repo in ${1}..." + # Let the user know what's happening + echo -ne " ${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 --force -d || true # Okay for already clean directory + git clean --quiet --force -d || true # Okay for already clean directory + # Pull the latest commits git pull --quiet &> /dev/null || return $? - echo " done!" + # Show a completion message + echo -e "${OVER} ${TICK} ${str}" + # Move back into the oiginal directory cd "${curdir}" &> /dev/null || return 1 return 0 } +# A function that combines the functions previously made getGitFiles() { - # Setup git repos for directory and repository passed - # as arguments 1 and 2 + # Setup named variables for the git repos + # We need the directory local directory="${1}" + # as well as the repo URL local remoteRepo="${2}" - echo ":::" - echo "::: Checking for existing repository..." + # A local varible containing the message to be displayed + local str="Check for existing repository in ${1}" + # Show the message + echo -ne " ${INFO} ${str}..." + # Check if the directory is a repository if is_repo "${directory}"; then - update_repo "${directory}" || { echo "*** Error: Could not update local repository. Contact support."; exit 1; } - echo " done!" + # Show that we're checking it + echo -e "${OVER} ${TICK} ${str}" + # Update the repo, returning an error message on failure + update_repo "${directory}" || { echo -e "\\n ${COL_LIGHT_RED}Error: Could not update local repository. Contact support.${COL_NC}"; exit 1; } + # If it's not a .git repo, else - make_repo "${directory}" "${remoteRepo}" || { echo "Unable to clone repository, please contact support"; exit 1; } - echo " done!" + # Show an error + echo -e "${OVER} ${CROSS} ${str}" + # Attempt to make the repository, showing an error on falure + make_repo "${directory}" "${remoteRepo}" || { echo -e "\\n ${COL_LIGHT_RED}Error: Could not update local repository. Contact support.${COL_NC}"; exit 1; } fi + # echo a blank line + echo "" + # and return success? return 0 } +# Reset a repo to get rid of any local changed resetRepo() { + # Use named varibles for arguments local directory="${1}" - + # Move into the directory cd "${directory}" &> /dev/null || return 1 - echo -n "::: Resetting repo in ${1}..." + # Store the message in a varible + str="Resetting repository within ${1}..." + # Show the message + echo -ne " ${INFO} ${str}" + # Use git to remove the local changes git reset --hard &> /dev/null || return $? - echo " done!" + # And show the status + echo -e "${OVER} ${TICK} ${str}" + # Returning success anyway? return 0 } +# We need to know the IPv4 information so we can effectively setup the DNS server +# Without this information, we won't know where to Pi-hole will be found find_IPv4_information() { + # Named, local variables local route - # Find IP used to route to outside world + # Find IP used to route to outside world by checking the the route to Google's public DNS server route=$(ip route get 8.8.8.8) + # Use awk to strip out just the interface device as it is used in future commands IPv4dev=$(awk '{for (i=1; i<=NF; i++) if ($i~/dev/) print $(i+1)}' <<< "${route}") + # Get just the IP address IPv4bare=$(awk '{print $7}' <<< "${route}") + # Append the CIDR notation to the IP address IPV4_ADDRESS=$(ip -o -f inet addr show | grep "${IPv4bare}" | awk '{print $4}' | awk 'END {print}') + # Get the default gateway (the way to reach the Internet) IPv4gw=$(awk '{print $3}' <<< "${route}") } +# Get available interfaces that are UP get_available_interfaces() { - # Get available UP interfaces. + # There may be more than one so it's all stored in a variable availableInterfaces=$(ip --oneline link show up | grep -v "lo" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1) } +# A function for displaying the dialogs the user sees when first running the installer welcomeDialogs() { - # Display the welcome dialog - whiptail --msgbox --backtitle "Welcome" --title "Pi-hole automated installer" "\n\nThis installer will transform your device into a network-wide ad blocker!" ${r} ${c} + # Display the welcome dialog using an approriately 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} - # Support for a part-time dev - 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} + # 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. + 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. - echo "::: Verifying free disk space..." + local str="Disk space check" + # Reqired space in KB local required_free_kilobytes=51200 - local existing_free_kilobytes=$(df -Pk | grep -m1 '\/$' | awk '{print $4}') + # Calculate existing free space on this machine + local existing_free_kilobytes + existing_free_kilobytes=$(df -Pk | grep -m1 '\/$' | awk '{print $4}') - # - Unknown free disk space , not a integer + # If the existing space is not an integer, if ! [[ "${existing_free_kilobytes}" =~ ^([0-9])+$ ]]; then - echo "::: Unknown free disk space!" - echo "::: We were unable to determine available free disk space on this system." - echo "::: You may override this check and force the installation, however, it is not recommended" - echo "::: To do so, pass the argument '--i_do_not_follow_recommendations' to the install script" - echo "::: eg. curl -L https://install.pi-hole.net | bash /dev/stdin --i_do_not_follow_recommendations" + # show an error that we can't determine the free space + echo -e " ${CROSS} ${str} + Unknown free disk space! + We were unable to determine available free disk space on this system. + You may override this check, however, it is not recommended + The option '${COL_LIGHT_RED}--i_do_not_follow_recommendations${COL_NC}' can override this + e.g: curl -L https://install.pi-hole.net | bash /dev/stdin ${COL_LIGHT_RED}