diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3014625b..23e67795 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -9,7 +9,7 @@ _{replace this text with a number from 1 to 10, with 1 being not familiar, and 10 being very familiar}_ --- -**[FEATURE REQUEST | QUESTION | OTHER]:** +**[BUG REPORT | OTHER]:** 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. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 424bbc78..8583806b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,17 +3,17 @@ ***Please submit all pull requests against the `development` branch. Failure to do so will delay or deny your request*** - [] 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. +- [] I have written tests and verified that they fail without my change. +- [] I have squashed any insignificant commits. +- [] This change has comments for package types, values, functions, and non-obvious lines of code. +- [] 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. +- [] I have Signed Off all commits. (`git commit --signoff`) -**How familiar are you with the codebase?:** +***Please explain what you have done and wish to accomplish with this Pull Request*** -_{replace this text with a number from 1 to 10, with 1 being not familiar, and 10 being very familiar}_ +1. What does this change do, exactly? ---- -_{replace this line with your pull request content}_ +2. Please link to the relevant issues. - -_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._ +3. Which documentation changes (if any) need to be made because of this PR? diff --git a/.pullapprove.yml b/.pullapprove.yml index 39566b34..30888234 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -10,7 +10,7 @@ group_defaults: reset_on_push: enabled: true reject_value: -2 - approve_regex: '^(Approved|:shipit:|:\+1:|Engage)' + approve_regex: '^(Approved|:shipit:|:\+1:|Engage|:taco:)' reject_regex: '^(Rejected|:-1:|Borg)' author_approval: auto: true @@ -35,4 +35,4 @@ groups: - master required: 4 teams: - - admin + - approvers diff --git a/LICENSE b/LICENSE index 9fddaad1..9ce6e5b0 100644 --- a/LICENSE +++ b/LICENSE @@ -12,81 +12,63 @@ This license applies to the whole project EXCEPT: The licenses that existed prior to this change have remained intact. ------------------------------------------------------------- +EUROPEAN UNION PUBLIC LICENCE v. 1.2 -European Union Public Licence -V. 1.1 +EUPL © the European Union 2007, 2016 -EUPL (C) the European Community 2007 - -This European Union Public Licence (the "EUPL") applies to the Work or Software (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). - -The Original Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Original Work: - -Licensed under the EUPL V.1.1 - -or has expressed by any other mean his willingness to license under the EUPL. +This European Union Public Licence (the EUPL) applies to the Work (as defined below) which is provided under the terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such use is covered by a right of the copyright holder of the Work). +The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following notice immediately following the copyright notice for the Work: +Licensed under the EUPL +or has expressed by any other means his willingness to license under the EUPL. 1. Definitions In this Licence, the following terms have the following meaning: - The Licence: this Licence. - -- The Original Work or the Software: the software distributed and/or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. - +- The Original Work: the work or software distributed or communicated by the Licensor under this Licence, available as Source Code and also as Executable Code as the case may be. - Derivative Works: the works or software that could be created by the Licensee, based upon the Original Work or modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in the country mentioned in Article 15. - -- The Work: the Original Work and/or its Derivative Works. - +- The Work: the Original Work or its Derivative Works. - The Source Code: the human-readable form of the Work which is the most convenient for people to study and modify. - - The Executable Code: any code which has generally been compiled and which is meant to be interpreted by a computer as a program. - -- The Licensor: the natural or legal person that distributes and/or communicates the Work under the Licence. - +- The Licensor: the natural or legal person that distributes or communicates the Work under the Licence. - Contributor(s): any natural or legal person who modifies the Work under the Licence, or otherwise contributes to the creation of a Derivative Work. - -- The Licensee or "You": any natural or legal person who makes any usage of the Software under the terms of the Licence. - -- Distribution and/or Communication: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, on-line or off-line, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. +- The Licensee or You: any natural or legal person who makes any usage of the Work under the terms of the Licence. +- Distribution or Communication: any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential functionalities at the disposal of any other natural or legal person. 2. Scope of the rights granted by the Licence -The Licensor hereby grants You a world-wide, royalty-free, non-exclusive, sub-licensable licence to do the following, for the duration of copyright vested in the Original Work: - +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for the duration of copyright vested in the Original Work: - use the Work in any circumstance and for all usage, - reproduce the Work, -- modify the Original Work, and make Derivative Works based upon the Work, +- modify the Work, and make Derivative Works based upon the Work, - communicate to the public, including the right to make available or display the Work or copies thereof to the public and perform publicly, as the case may be, the Work, - distribute the Work or copies thereof, - lend and rent the Work or copies thereof, -- sub-license rights in the Work or copies thereof. - +- sublicense rights in the Work or copies thereof. Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the applicable law permits so. - In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed by law in order to make effective the licence of the economic rights here above listed. - -The Licensor grants to the Licensee royalty-free, non exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the extent necessary to make use of the rights granted on the Work under this Licence. 3. Communication of the Source Code -The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute and/or communicate the Work. +The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to distribute or communicate the Work. 4. Limitations on copyright -Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Original Work or Software, of the exhaustion of those rights or of other applicable limitations thereto. +Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations thereto. 5. Obligations of the Licensee The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those obligations are the following: -Attribution right: the Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes and/or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. +Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work to carry prominent notices stating that the Work has been modified and the date of modification. -Copyleft clause: If the Licensee distributes and/or communicates copies of the Original Works or Derivative Works based upon the Original Work, this Distribution and/or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. +Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless the Original Work is expressly distributed only under this version of the Licence - for example by communicating EUPL v. 1.2 only. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the Work or Derivative Work that alter or restrict the terms of the Licence. -Compatibility clause: If the Licensee Distributes and/or Communicates Derivative Works or copies thereof based upon both the Original Work and another work licensed under a Compatible Licence, this Distribution and/or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, "Compatible Licence" refers to the licences listed in the appendix attached to this Licence. Should the Licensee’s obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. +Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done under the terms of this Compatible Licence. For the sake of this clause, Compatible Licence refers to the licences listed in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. -Provision of Source Code: When distributing and/or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute and/or communicate the Work. +Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available for as long as the Licensee continues to distribute or communicate the Work. Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the copyright notice. @@ -100,10 +82,8 @@ Each time You accept the Licence, the original Licensor and subsequent Contribut 7. Disclaimer of Warranty -The Work is a work in progress, which is continuously improved by numerous contributors. It is not a finished work and may therefore contain defects or "bugs" inherent to this type of software development. - -For the above reason, the Work is provided under the Licence on an "as is" basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. - +The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work and may therefore contain defects or bugs inherent to this type of development. +For the above reason, the Work is provided under the Licence on an as is basis and without warranties of any kind concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this Licence. This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. 8. Disclaimer of Liability @@ -112,56 +92,55 @@ Except in the cases of wilful misconduct or damages directly caused to natural p 9. Additional agreements -While distributing the Original Work or Derivative Works, You may choose to conclude an additional agreement to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or services consistent with this Licence. However, in accepting such obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any such warranty or additional liability. +While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by the fact You have accepted any warranty or additional liability. 10. Acceptance of the Licence -The provisions of this Licence can be accepted by clicking on an icon "I agree" placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. - -Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution and/or Communication by You of the Work or copies thereof. +The provisions of this Licence can be accepted by clicking on an icon I agree placed under the bottom of a window displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms and conditions. +Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution or Communication by You of the Work or copies thereof. 11. Information to the public -In case of any Distribution and/or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. +In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, by offering to download the Work from a remote location) the distribution channel or media (for example, a website) must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence and the way it may be accessible, concluded, stored and reproduced by the Licensee. 12. Termination of the Licence The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms of the Licence. - Such a termination will not terminate the licences of any person who has received the Work from the Licensee under the Licence, provided such persons remain in full compliance with the Licence. 13. Miscellaneous -Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work licensed hereunder. - -If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed and/or reformed so as necessary to make it valid and enforceable. - -The European Commission may publish other linguistic versions and/or new versions of this Licence, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. - +Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the Work. +If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid and enforceable. +The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. New versions of the Licence will be published with a unique version number. All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take advantage of the linguistic version of their choice. 14. Jurisdiction -Any litigation resulting from the interpretation of this License, arising between the European Commission, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Communities, as laid down in article 238 of the Treaty establishing the European Community. - -Any litigation arising between Parties, other than the European Commission, and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. +Without prejudice to specific agreement between parties, +- any litigation resulting from the interpretation of this License, arising between the European Union institutions, bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, +- any litigation arising between other parties and resulting from the interpretation of this License, will be subject to the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. 15. Applicable Law -This Licence shall be governed by the law of the European Union country where the Licensor resides or has his registered office. - -This licence shall be governed by the Belgian law if: - -- a litigation arises between the European Commission, as a Licensor, and any Licensee; -- the Licensor, other than the European Commission, has no residence or registered office inside a European Union country. - +Without prejudice to specific agreement between parties, +- this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, resides or has his registered office, +- this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside a European Union Member State. === + Appendix -"Compatible Licences" according to article 5 EUPL are: -- GNU General Public License (GNU GPL) v. 2 +Compatible Licences according to Article 5 EUPL are: +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 - Open Software License (OSL) v. 2.1, v. 3.0 -- Common Public License v. 1.0 -- Eclipse Public License v. 1.0 -- Cecill v. 2.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence - Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+) +- The European Commission may update this Appendix to later versions of the above licences without producing a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the covered Source Code from exclusive appropriation. +- All other changes or additions to this Appendix require the production of a new EUPL version. diff --git a/README.md b/README.md index 75c548e5..4a6a86c6 100644 --- a/README.md +++ b/README.md @@ -8,37 +8,37 @@

-## The multi-platform, network-wide ad blocker +## Pi-hole®: The multi-platform, network-wide ad blocker -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. - -- Web Browsers -- Cell Phones -- Smart TV's -- Internet-connected home automation -- Anything that communicates with the Internet +Block ads for **all** your devices _without_ the need to install client-side software.

-## Your Support Still Matters +## Executive Summary +The Pi-hole blocks ads at the DNS-level, so all your devices are protected. -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. +- **Easy-to-install** - our intelligent installer walks you through the process with no additional software needed on client devices +- **Universal** - ads are blocked in _non-browser locations_ such as ad-supported mobile apps and smart TVs +- **Quick** - installation takes less than ten minutes and it [_really_ is _that easy_](https://discourse.pi-hole.net/t/new-pi-hole-questions/3971/5?u=jacob.salmela) +- **Informative** - an administrative Web interface shows ad-blocking statistics +- **Lightweight** - designed to run on [minimal resources](https://discourse.pi-hole.net/t/hardware-software-requirements/273) +- **Scalable** - even in large environments, [Pi-hole can handle hundreds of millions of queries](https://pi-hole.net/2017/05/24/how-much-traffic-can-pi-hole-handle/) (with the right hardware specs) +- **Powerful** - advertisements are blocked over IPv4 _and_ IPv6 +- **Fast** - it speeds up high-cost, high-latency networks by caching DNS queries and saves bandwidth by not downloading advertisement elements +- **Versatile** - Pi-hole can function also function as a DHCP server -- ![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 +# 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` +#### `curl -sSL https://install.pi-hole.net | bash` -#### Alternative Semi-Automated Install Methods +## 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. +### Clone our repository and run the automated installer from your device. ``` git clone --depth 1 https://github.com/pi-hole/pi-hole.git Pi-hole @@ -53,91 +53,233 @@ wget -O basic-install.sh https://install.pi-hole.net 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. +Once installed, [configure your router to have **DHCP clients use the 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) and then any device that connects to your network will have ads blocked without any further configuration. -## What is Pi-hole™ and how do I install it? +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. + +Alternatively, you can manually set each device to use Pi-hole as their DNS server. + +# What is Pi-hole and how do I install it?

+# Pi-hole Is Free, But Powered By Your Donations -## Get Help Or Connect With Us On The Web +[Digital Ocean](http://www.digitalocean.com/?refcode=344d234950e1) helps with our infrastructure, but [our developers](https://github.com/orgs/pi-hole/people) are all volunteers so *your donations help keep us innovating*. + +- ![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 + +## Other Ways To Support Us +### Affiliate Links +If you'd rather not send money, there are [other ways to support us](https://pi-hole.net/donate): you can sign up for services through our affiliate links, which will also help us offset some of the costs associated with keeping Pi-hole operational; or you can support us in some non-tangible ways as listed below. + +### Contributing Code Via Pull Requests + +We don't work on Pi-hole for monetary reasons; we work on it because we think it's fun and we think our software is important in today's world. To that end, we welcome all contributors--from novices to masters. + +If you feel you have some code to contribute, we're happy to take a look. Just make sure to fill out our template when submitting a pull request. We're all volunteers on the project and without all the information in the template, it's very difficult for us to quickly get the code merged in. + +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. These are two important scripts but we think they can also be a valuable resource to those who want to learn how to write scripts or code a program, which is why they are fully commented. So we encourage anyone who likes to tinker to read through it and submit a PR for us to review. + +### Presenting About Pi-hole + +Word-of-mouth has immensely helped our project grow. If you are going to be presenting about Pi-hole at a conference, meetup, or even for a school project, [get a hold of us for some free swag](https://pi-hole.net/2017/05/17/giving-a-presentation-on-pi-hole-contact-us-first-for-some-goodies-and-support/) to hand out to your audience. + +# Overview Of Features + +## The Dashboard (Web Interface) + +The [dashboard](https://github.com/pi-hole/AdminLTE#pi-hole-admin-dashboard) will (by default) be enabled during installation so you can view stats, change settings, and configure your Pi-hole. + +![Pi-hole Dashboard](https://assets.pi-hole.net/static/dashboard.png) + +There are several ways to [access the dashboard](https://discourse.pi-hole.net/t/how-do-i-access-pi-holes-dashboard-admin-interface/3168): + +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) + +### The Query Log + +If enabled, the query log will show all of the DNS queries requested by clients using Pi-hole as their DNS server. Forwarded domains will show in green, and blocked (_Pi-holed_) domains will show in red. You can also white or black list domains from within this section. + +

+ +

+ +The query log and graphs are what have helped people [discover what sort of traffic is traversing their networks](https://pi-hole.net/2017/07/06/round-3-what-really-happens-on-your-network/). + +#### Long-term Statistics +Using our Faster-Than-Light Engine ([FTL](https://github.com/pi-hole/FTL)), Pi-hole can store all of the domains queried in a database for retrieval or analysis later on. You can view this data as a graph, individual queries, or top clients/advertisers. + +

+ +

+ +### Whitelist And Blacklist + +Domains can be [whitelisted](https://discourse.pi-hole.net/t/commonly-whitelisted-domains/212) and/or [blacklisted](https://discourse.pi-hole.net/t/commonly-blacklisted-domains/305) using either the dashboard or [the `pihole` command](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738). + +

+ +

+ +#### Additional Blocklists +By default, Pi-hole blocks over 100,000 known ad-serving domains. You can expand the blocking power of your Pi-hole by [adding additional lists](https://discourse.pi-hole.net/t/how-do-i-add-additional-block-lists-to-pi-hole/259) such as the ones found on [The Big Blocklist Collection](https://wally3k.github.io/). + +

+ +

+ +### Enable And Disable Pi-hole +Sometimes you may want to stop using Pi-hole or turn it back on. You can trigger this via the dashboard or command line. + +

+ +

+ +### Tools + +

+ +

+ + +#### Update Ad Lists +This runs `gravity` to download any newly-added domains from your source lists. + +#### Query Ad Lists +You can find out what list a certain domain was on. This is useful for troubleshooting sites that may not work properly due to a blocked domain. + +#### `tail`ing Log Files +You can [watch the log files](https://discourse.pi-hole.net/t/how-do-i-watch-and-interpret-the-pihole-log-file/276) in real time to help debug any issues, or just see what's happening with your Pi-hole. + +#### Pi-hole Debugger +If you are having trouble with your Pi-hole, this is the place to go. You can run the debugger and it will attempt to diagnose any issues and then link to an FAQ with instructions on rectifying the problem. + +

+ +

+ +If run [via the command line](https://discourse.pi-hole.net/t/the-pihole-command-with-examples/738#debug), you will see red/yellow/green text, which makes it easy to identify any problems. + +

+ +

+ + +After the debugger has finished, you have the option to upload it to our secure server for 48 hours. All you need to do then is provide one of our developers the unique token generated by the debugger (this is usually done via [our forums](https://discourse.pi-hole.net/c/bugs-problems-issues)). + +

+ +

+ +However, most of the time, you will be able to solve any issues without any intervention from us. But if you can't, we're always around to help out. + +### Settings + +The settings page lets you control and configure your Pi-hole. You can do things like: + +- view networking information +- flush logs or disable the logging of queries +- [enable 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) +- [manage block lists](https://discourse.pi-hole.net/t/how-do-i-add-additional-block-lists-to-pi-hole/259) +- exclude domains from the graphs and enable privacy options +- configure upstream DNS servers +- restart Pi-hole's services +- back up some of Pi-hole's important files +- and more! + +

+ +

+ + +## Built-in DHCP Server + +Pi-hole ships with a [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). 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. + +One nice feature of using Pi-hole's DHCP server if you can set hostnames and DHCP reservations so you'll [see hostnames in the query log instead of IP addresses](https://discourse.pi-hole.net/t/how-do-i-show-hostnames-instead-of-ip-addresses-in-the-dashboard/3530). You can still do this without using Pi-hole's DHCP server; it just takes a little more work. If you do plan to use Pi-hole's DHCP server, be sure to disable DHCP on your router first. + +

+ +

+ +## The FTL Engine: Our API + +A read-only API can be accessed at `admin/api.php` (the same output can be achieved on the CLI by running `pihole -c -j`). + +It returns the following JSON: +``` json +{ + "domains_being_blocked":111175, + "dns_queries_today":15669, + "ads_blocked_today":1752, + "ads_percentage_today":11.181314, + "unique_domains":1178, + "queries_forwarded":9177, + "queries_cached":4740, + "unique_clients":18 + } +``` + +More details on the API can be found [here](https://discourse.pi-hole.net/t/pi-hole-api/1863) and on [the repo itself](https://github.com/pi-hole/FTL). + +### Real-time Statistics, Courtesy Of The Time Cops + +Using [chronometer2](https://github.com/pi-hole/pi-hole/blob/master/advanced/Scripts/chronometer.sh), 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 LCD screen such as the [2.8" LCD screen from Adafruit](http://amzn.to/1P0q1Fj). + +Simply run `pihole -c` for some detailed information. +``` +|¯¯¯(¯)__|¯|_ ___|¯|___ Pi-hole: v3.2 +| ¯_/¯|__| ' \/ _ \ / -_) AdminLTE: v3.2 +|_| |_| |_||_\___/_\___| FTL: v2.10 + —————————————————————————————————————————————————————————— + Hostname: pihole (Raspberry Pi 1, Model B) + Uptime: 11 days, 12:55:01 + Task Load: 0.35 0.16 0.15 (Active: 5 of 33 tasks) + CPU usage: 48% (1 core @ 700 MHz, 47c) + RAM usage: 12% (Used: 54 MB of 434 MB) + HDD usage: 20% (Used: 1 GB of 7 GB) + LAN addr: 192.168.1.100 (Gateway: 192.168.1.1) + Pi-hole: Active (Blocking: 111175 sites) + Ads Today: 11% (1759 of 15812 queries) + Fwd DNS: 208.67.222.222 (Alt DNS: 3 others) + —————————————————————————————————————————————————————————— + Recently blocked: www.google-analytics.com + Top Advertiser: www.example.org + Top Domain: www.example.org + Top Client: somehost +``` + +

+ +

+ +

+ +

+ +# Get Help Or Connect With Us On The Web - [Users Forum](https://discourse.pi-hole.net/) - [FAQs](https://discourse.pi-hole.net/c/faqs) +- [Feature requests](https://discourse.pi-hole.net/c/feature-requests?order=votes) - [Wiki](https://github.com/pi-hole/pi-hole/wiki) +- [Facebook](https://www.facebook.com/ThePiHole/) - ![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 +# Technical Details -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. - -### Gravity - -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`. +To summarize into a short sentence, the Pi-hole is an **advertising-aware DNS/Web server**. And 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 it was 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. - -#### Other Operating Systems - -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. - -### Web Interface - -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: - -`http://192.168.1.x/admin/index.php` or `http://pi.hole/admin` - -![Pi-hole Advanced Stats Dashboard](https://assets.pi-hole.net/static/dashboard212.png) - -### Whitelist and blacklist - -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: - -- enable Pi-hole's built-in DHCP server -- exclude domains from the graphs -- configure upstream DNS servers -- and more! - -![Settings page](https://assets.pi-hole.net/static/settings212.png) - -#### Built-in DHCP Server - -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. -

- -

- -## API - -A basic read-only API can be accessed at `/admin/api.php`. It returns the following JSON: - -``` json -{ - "domains_being_blocked": "136708", - "dns_queries_today": "18108", - "ads_blocked_today": "14648", - "ads_percentage_today": "80.89" -} -``` - -The same output can be achieved on the CLI by running `chronometer.sh -j` - -## Real-time Statistics - -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) - -## Pi-hole™ Projects +# Pi-hole Projects - [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) @@ -156,8 +298,9 @@ You can view [real-time stats](https://discourse.pi-hole.net/t/how-do-i-view-my- - [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) -## Coverage +# 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) diff --git a/advanced/01-pihole.conf b/advanced/01-pihole.conf index 0ddf1caa..8b772ae8 100644 --- a/advanced/01-pihole.conf +++ b/advanced/01-pihole.conf @@ -21,10 +21,13 @@ ############################################################################### addn-hosts=/etc/pihole/gravity.list +addn-hosts=/etc/pihole/black.list addn-hosts=/etc/pihole/local.list domain-needed +localise-queries + bogus-priv no-resolv diff --git a/advanced/Scripts/COL_TABLE b/advanced/Scripts/COL_TABLE new file mode 100644 index 00000000..20dd98b0 --- /dev/null +++ b/advanced/Scripts/COL_TABLE @@ -0,0 +1,28 @@ +if [[ -t 1 ]] && [[ $(tput colors) -ge 8 ]]; then + COL_NC='' + COL_WHITE='' + COL_BLACK='' + COL_BLUE='' + COL_LIGHT_BLUE='' + COL_GREEN='' + COL_LIGHT_GREEN='' + COL_CYAN='' + COL_LIGHT_CYAN='' + COL_RED='' + COL_LIGHT_RED='' + COL_URG_RED='' + COL_PURPLE='' + COL_LIGHT_PURPLE='' + COL_BROWN='' + COL_YELLOW='' + COL_GRAY='' + COL_LIGHT_GRAY='' + COL_DARK_GRAY='' +fi + +TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]" +CROSS="[${COL_LIGHT_RED}✗${COL_NC}]" +INFO="[i]" +QST="[?]" +DONE="${COL_LIGHT_GREEN} done!${COL_NC}" +OVER="\r\033[K" diff --git a/advanced/Scripts/chronometer.sh b/advanced/Scripts/chronometer.sh index 67ff495b..a9ccf900 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,102 +8,565 @@ # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. +LC_NUMERIC=C -#Functions############################################################################################################## -piLog="/var/log/pihole.log" -gravity="/etc/pihole/gravity.list" - -. /etc/pihole/setupVars.conf - -function GetFTLData { +# Retrieve stats from FTL engine +pihole-FTL() { + ftl_port=$(cat /var/run/pihole-FTL.port 2> /dev/null) + if [[ -n "$ftl_port" ]]; then # Open connection to FTL - exec 3<>/dev/tcp/localhost/"$(cat /var/run/pihole-FTL.port)" + exec 3<>"/dev/tcp/localhost/$ftl_port" # Test if connection is open - if { >&3; } 2> /dev/null; then - # Send command to FTL - echo -e ">$1" >&3 + if { "true" >&3; } 2> /dev/null; then + # Send command to FTL + echo -e ">$1" >&3 - # Read input - read -r -t 1 LINE <&3 - until [ ! $? ] || [[ "$LINE" == *"EOM"* ]]; do - echo "$LINE" >&1 - read -r -t 1 LINE <&3 - done + # Read input + read -r -t 1 LINE <&3 + until [[ ! $? ]] || [[ "$LINE" == *"EOM"* ]]; do + echo "$LINE" >&1 + read -r -t 1 LINE <&3 + done - # Close connection - exec 3>&- - exec 3<&- + # Close connection + exec 3>&- + exec 3<&- fi + else + echo "0" + fi } -outputJSON() { - get_summary_data - echo "{\"domains_being_blocked\":${domains_being_blocked_raw},\"dns_queries_today\":${dns_queries_today_raw},\"ads_blocked_today\":${ads_blocked_today_raw},\"ads_percentage_today\":${ads_percentage_today_raw}}" +# Print spaces to align right-side additional text +printFunc() { + local text_last + + 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 } -get_summary_data() { - local summary=$(GetFTLData "stats") - domains_being_blocked_raw=$(grep "domains_being_blocked" <<< "${summary}" | grep -Eo "[0-9]+$") - domains_being_blocked=$(printf "%'.f" ${domains_being_blocked_raw}) - dns_queries_today_raw=$(grep "dns_queries_today" <<< "$summary" | grep -Eo "[0-9]+$") - dns_queries_today=$(printf "%'.f" ${dns_queries_today_raw}) - ads_blocked_today_raw=$(grep "ads_blocked_today" <<< "$summary" | grep -Eo "[0-9]+$") - ads_blocked_today=$(printf "%'.f" ${ads_blocked_today_raw}) - ads_percentage_today_raw=$(grep "ads_percentage_today" <<< "$summary" | grep -Eo "[0-9.]+$") - LC_NUMERIC=C ads_percentage_today=$(printf "%'.f" ${ads_percentage_today_raw}) +# Perform on first Chrono run (not for JSON formatted string) +get_init_stats() { + calcFunc(){ awk "BEGIN {print $*}" 2> /dev/null; } + + # Convert bytes to human-readable format + hrBytes() { + awk '{ + num=$1; + if(num==0) { + print "0 B" + } else { + xxx=(num<0?-num:num) + sss=(num<0?-1:1) + split("B KB MB GB TB PB",type) + for(i=5;yyy < 1;i--) { + yyy=xxx / (2^(10*i)) + } + printf "%.0f " type[i+2], yyy*sss + } + }' <<< "$1"; + } + + # Convert seconds to human-readable format + hrSecs() { + 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" + } + + # 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="" + fi + + # Get RPi throttle state (RPi 3B only) & model number, or OS distro info + if command -v vcgencmd &> /dev/null; then + 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 + 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 + a0104[0-1]|a21041|a22042) sys_model=" 2, Model B";; # 1GB + 900021) sys_model=" 1, Model A+";; # 512MB + 900032) sys_model=" 1, Model B+";; # 512MB + 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="";; + esac + sys_type="Raspberry Pi$sys_model" + else + source "/etc/os-release" + CODENAME=$(sed 's/[()]//g' <<< "${VERSION/* /}") + sys_type="${NAME/ */} ${CODENAME^} $VERSION_ID" + fi + + # Get core count + sys_cores=$(grep -c "^processor" /proc/cpuinfo) + + # Test existence of clock speed file for ARM CPU + if [[ -f "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" ]]; then + scaling_freq_file="/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" + fi + + # Test existence of temperature file + if [[ -f "/sys/class/thermal/thermal_zone0/temp" ]]; then + temp_file="/sys/class/thermal/thermal_zone0/temp" + elif [[ -f "/sys/class/hwmon/hwmon0/temp1_input" ]]; then + temp_file="/sys/class/hwmon/hwmon0/temp1_input" + else + temp_file="" + fi + + # Test existence of setupVars config + if [[ -f "/etc/pihole/setupVars.conf" ]]; then + setupVars="/etc/pihole/setupVars.conf" + fi } -normalChrono() { - for (( ; ; )); do - get_summary_data - domain=$(GetFTLData recentBlocked) - clear - # Displays a colorful Pi-hole logo - echo " ___ _ _ _" - echo "| _ (_)___| |_ ___| |___" - echo "| _/ |___| ' \/ _ \ / -_)" - echo "|_| |_| |_||_\___/_\___|" - echo "" - echo " ${IPV4_ADDRESS}" - echo "" - uptime | cut -d' ' -f11- - #uptime -p #Doesn't work on all versions of uptime - uptime | awk -F'( |,|:)+' '{if ($7=="min") m=$6; else {if ($7~/^day/) {d=$6;h=$8;m=$9} else {h=$6;m=$7}}} {print d+0,"days,",h+0,"hours,",m+0,"minutes."}' - echo "-------------------------------" - echo "Recently blocked:" - echo " $domain" +get_sys_stats() { + local ph_ver_raw + local cpu_raw + local ram_raw + local disk_raw - echo "Blocking: ${domains_being_blocked}" - echo "Queries: ${dns_queries_today}" - echo "Pi-holed: ${ads_blocked_today} (${ads_percentage_today}%)" + # 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" - sleep 5 - done + 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="-1" + fi + + sys_name=$(hostname) + + [[ -n "$TEMPERATUREUNIT" ]] && temp_unit="$TEMPERATUREUNIT" || temp_unit="c" + + # Get storage stats for partition mounted on / + read -r -a disk_raw <<< "$(df -B1 / 2> /dev/null | awk 'END{ print $3,$2,$5 }')" + 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_max=$(( ${DHCP_END##*.} - ${DHCP_START##*.} + 1 )) + 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% 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 $2;exit}') + cpu_mhz=$(printf "%.0f" "${cpu_mhz//[[:space:]]/}") + fi + + # 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" + fi + + # Determine colour for temperature + if [[ -n "$temp_file" ]]; then + if [[ "$temp_unit" == "C" ]]; then + 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="";; + 5[0-9]) cpu_col="$COL_YELLOW";; + 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" + + elif [[ "$temp_unit" == "F" ]]; then + 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="";; + 1[2-3][0-9]) cpu_col="$COL_YELLOW";; + 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" + + else + cpu_temp_str=$(printf " @ %.0fk\\n" "$(calcFunc "($(< $temp_file) / 1000) + 273.15")") + fi + else + cpu_temp_str="" + fi + + 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}Offline" + fi + + if [[ "$DHCP_ACTIVE" == "true" ]]; then + 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 } -displayHelp() { - cat << EOM -::: Displays stats about your piHole! -::: -::: Usage: sudo pihole -c [optional:-j] -::: Note: If no option is passed, then stats are displayed on screen, updated every 5 seconds -::: -::: Options: -::: -j, --json output stats as JSON formatted string -::: -h, --help display this help text -EOM - exit 0 +get_ftl_stats() { + local stats_raw + + 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 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}") + 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}Pi-hole" + lte_str=" ${COL_DARK_GRAY}Admin" + 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}PH" + 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 + get_strings + + # 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 + + # 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 [[ "$*" == *"-r"* ]]; then + sleep "$num" + else + sleep 5 + fi + fi + + done +} + +jsonFunc() { + get_ftl_stats "json" + echo "{\"domains_being_blocked\":${domains_being_blocked_raw},\"dns_queries_today\":${dns_queries_today_raw},\"ads_blocked_today\":${ads_blocked_today_raw},\"ads_percentage_today\":${ads_percentage_today_raw}}" +} + +helpFunc() { + if [[ "$1" == "?" ]]; then + echo "Unknown option. Please view 'pihole -c --help' for more information" + else + 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 } if [[ $# = 0 ]]; then - normalChrono + chronoFunc fi for var in "$@"; do - case "$var" in - "-j" | "--json" ) outputJSON;; - "-h" | "--help" ) displayHelp;; - * ) exit 1;; - esac + case "$var" in + "-j" | "--json" ) jsonFunc;; + "-h" | "--help" ) helpFunc;; + "-r" | "--refresh" ) chronoFunc "$@";; + "-e" | "--exit" ) chronoFunc "$@";; + * ) helpFunc "?";; + esac done diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index 537ebac3..86589083 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -3,14 +3,12 @@ # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # -# Whitelists and blacklists domains +# Whitelist and blacklist domains # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. - - -#globals +# Globals basename=pihole piholeDir=/etc/${basename} whitelist=${piholeDir}/whitelist.txt @@ -26,209 +24,225 @@ domToRemoveList=() listMain="" listAlt="" +colfile="/opt/pihole/COL_TABLE" +source ${colfile} + + helpFunc() { + if [[ "${listMain}" == "${whitelist}" ]]; then + param="w" + type="white" + elif [[ "${listMain}" == "${wildcardlist}" ]]; then + param="wild" + type="wildcard black" + else + param="b" + type="black" + fi - if [[ ${listMain} == ${whitelist} ]]; then - letter="w" - word="white" - else - letter="b" - word="black" - fi + echo "Usage: pihole -${param} [options] +Example: 'pihole -${param} site.com', or 'pihole -${param} site1.com site2.com' +${type^}list one or more domains - cat << EOM -::: Immediately ${word}lists one or more domains in the hosts file -::: -::: Usage: pihole -${letter} domain1 [domain2 ...] -::: -::: Options: -::: -d, --delmode Remove domains from the ${word}list -::: -nr, --noreload Update ${word}list without refreshing dnsmasq -::: -q, --quiet Output is less verbose -::: -h, --help Show this help dialog -::: -l, --list Display your ${word}listed domains -EOM -if [[ "${letter}" == "b" ]]; then - echo "::: -wild, --wildcard Add wildcard entry (only blacklist)" -fi - exit 0 +Options: + -d, --delmode Remove domain(s) from the ${type}list + -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" + + exit 0 } EscapeRegexp() { - # This way we may safely insert an arbitrary - # string in our regular expressions - # Also remove leading "." if present - echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" + # This way we may safely insert an arbitrary + # string in our regular expressions + # Also remove leading "." if present + echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" } -HandleOther(){ - # First, convert everything to lowercase - domain=$(sed -e "y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/" <<< "$1") +HandleOther() { + # 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 - domList=("${domList[@]}" ${validDomain}) - fi + # Check validity of domain + validDomain=$(perl -lne 'print if /^((-|_)*[a-z\d]((-|_)*[a-z\d])*(-|_)*)(\.(-|_)*([a-z\d]((-|_)*[a-z\d])*))*$/' <<< "${domain}") # Valid chars check + validDomain=$(perl -lne 'print if /^.{1,253}$/' <<< "${validDomain}") # Overall length check + validDomain=$(perl -lne 'print if /^[^\.]{1,63}(\.[^\.]{1,63})*$/' <<< "${validDomain}") # Length of each label + + if [[ -z "${validDomain}" ]]; then + echo -e " ${CROSS} $1 is not a valid argument or domain name!" + else + echo -e " ${TICK} $1 is a valid domain name!" + domList=("${domList[@]}" ${validDomain}) + fi } PoplistFile() { - #check whitelist file exists, and if not, create it - if [[ ! -f ${whitelist} ]]; then - touch ${whitelist} - fi - for dom in "${domList[@]}"; do - # Logic : If addmode then add to desired list and remove from the other; if delmode then remove from desired list but do not add to the other - if ${addmode}; then - AddDomain "${dom}" "${listMain}" - RemoveDomain "${dom}" "${listAlt}" - if [[ "${listMain}" == "${whitelist}" || "${listMain}" == "${blacklist}" ]]; then - RemoveDomain "${dom}" "${wildcardlist}" - fi - else - RemoveDomain "${dom}" "${listMain}" - fi - done + # Check whitelist file exists, and if not, create it + if [[ ! -f ${whitelist} ]]; then + touch ${whitelist} + fi + + for dom in "${domList[@]}"; do + # Logic: If addmode then add to desired list and remove from the other; if delmode then remove from desired list but do not add to the other + if ${addmode}; then + AddDomain "${dom}" "${listMain}" + RemoveDomain "${dom}" "${listAlt}" + if [[ "${listMain}" == "${whitelist}" || "${listMain}" == "${blacklist}" ]]; then + RemoveDomain "${dom}" "${wildcardlist}" + fi + else + RemoveDomain "${dom}" "${listMain}" + fi + done } AddDomain() { - list="$2" - domain=$(EscapeRegexp "$1") + list="$2" + domain=$(EscapeRegexp "$1") - if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then + [[ "${list}" == "${whitelist}" ]] && listname="whitelist" + [[ "${list}" == "${blacklist}" ]] && listname="blacklist" + [[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist" - 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 + if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then + 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 - if [[ "${bool}" == false ]]; then - #domain not found in the whitelist file, add it! - if [[ "${verbose}" == true ]]; then - echo "::: Adding $1 to $list..." - 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!" - fi - fi + if [[ "${bool}" == false ]]; then + # Domain not found in the whitelist file, add it! + if [[ "${verbose}" == true ]]; then + 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 -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 IP addresses + IPV4_ADDRESS=${IPV4_ADDRESS%/*} + IPV6_ADDRESS=${IPV6_ADDRESS%/*} - elif [[ "${list}" == "${wildcardlist}" ]]; then + bool=true + # Is the domain in the list? + grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false - source "${piholeDir}/setupVars.conf" - #Remove the /* from the end of the IPv4addr. - IPV4_ADDRESS=${IPV4_ADDRESS%/*} - IPV6_ADDRESS=${IPV6_ADDRESS} - - 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..." - fi - reload=true - 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!" - fi - fi - fi + if [[ "${bool}" == false ]]; then + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} Adding $1 to wildcard blacklist..." + fi + reload=true + echo "address=/$1/${IPV4_ADDRESS}" >> "${wildcardlist}" + if [[ "${#IPV6_ADDRESS}" > 0 ]]; then + echo "address=/$1/${IPV6_ADDRESS}" >> "${wildcardlist}" + fi + else + if [[ "${verbose}" == true ]]; then + 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 + [[ "${list}" == "${whitelist}" ]] && listname="whitelist" + [[ "${list}" == "${blacklist}" ]] && listname="blacklist" + [[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist" - 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 - fi + 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 -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 + 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 } Reload() { - # Reload hosts file - pihole -g -sd + # Reload hosts file + echo "" + echo -e " ${INFO} Updating gravity..." + echo "" + pihole -g -sd } 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" + 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}" - exit 0; + verbose=false + echo -e "Displaying $string:\n" + count=1 + while IFS= read -r RD; do + echo " ${count}: ${RD}" + count=$((count+1)) + done < "${listMain}" + else + echo -e " ${COL_LIGHT_RED}${listMain} does not exist!${COL_NC}" + fi + exit 0; } for var in "$@"; do - case "${var}" in - "-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";; - "-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";; - "-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;; - * ) HandleOther "${var}";; - esac + case "${var}" in + "-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";; + "-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";; + "-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;; + * ) HandleOther "${var}";; + esac done shift if [[ $# = 0 ]]; then - helpFunc + helpFunc fi PoplistFile if ${reload}; then - Reload + Reload fi diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index 09f20d6b..f214d2ce 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. # -# Checkout other branches than master +# 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 @@ -18,9 +19,103 @@ PH_TEST="true" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" # setupVars set in basic-install.sh source "${setupVars}" - update="false" +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 local directory="${1}" @@ -35,91 +130,105 @@ fully_fetch_repo() { return 0 } -get_available_branches(){ +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 --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 + git checkout "${branch}" --quiet || return 1 - 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 "::: Note that changing the branch is a severe change of your Pi-hole system." - echo "::: This is not supported unless one of the developers explicitly asks you to do this!" - read -r -p "::: Have you read and understood this? [y/N] " response - case ${response} in + echo " Please note that changing branches severely alters your Pi-hole subsystems" + echo " Features that work on the master branch, may not on a development branch" + echo -e " ${COL_LIGHT_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}" + read -r -p " Have you read and understood this? [y/N] " response + case "${response}" in [yY][eE][sS]|[yY]) - echo "::: Continuing." + echo "" return 0 ;; *) - echo "::: Aborting." + echo -e "\\n ${INFO} Branch change has been cancelled" return 1 ;; esac } -checkout() -{ +checkout() { local corebranches local webbranches # 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 @@ -129,74 +238,118 @@ 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 " ${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 " ${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 + exit 0 else - echo "Unable to complete update, contact Pi-hole" - exit 1 + 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 945cd81c..ea387b5a 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -9,521 +9,1124 @@ # 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" +# 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_YELLOW='\e[1;33m' + COL_LIGHT_PURPLE='\e[1;35m' + COL_CYAN='\e[0;36m' + TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]" + CROSS="[${COL_LIGHT_RED}✗${COL_NC}]" + INFO="[i]" + DONE="${COL_LIGHT_GREEN} done!${COL_NC}" + 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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_GREEN}${os_to_check}${COL_NC}";; + "Ubuntu") log_write "${TICK} ${COL_LIGHT_GREEN}${os_to_check}${COL_NC}";; + "Fedora") log_write "${TICK} ${COL_LIGHT_GREEN}${os_to_check}${COL_NC}";; + "Debian") log_write "${TICK} ${COL_LIGHT_GREEN}${os_to_check}${COL_NC}";; + "CentOS") log_write "${TICK} ${COL_LIGHT_GREEN}${os_to_check}${COL_NC}";; + # If not, show it in red and link to our software requirements page + *) log_write "${CROSS} ${COL_LIGHT_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_LIGHT_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}" } 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_LIGHT_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_LIGHT_GREEN}${PROCESSOR}${COL_NC}" + ;; + "armv6l") "${TICK} ${COL_LIGHT_GREEN}${PROCESSOR}${COL_NC}" + ;; + "armv6") "${TICK} ${COL_LIGHT_GREEN}${PROCESSOR}${COL_NC}" + ;; + "armv7l") "${TICK} ${COL_LIGHT_GREEN}${PROCESSOR}${COL_NC}" + ;; + "aarch64") "${TICK} ${COL_LIGHT_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_LIGHT_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 - echo "Gateway did not respond." - return 1 - else - 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 - echo "Query did not respond." - return 1 - else - 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=$(cat ${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_LIGHT_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_LIGHT_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_LIGHT_GREEN}${ip_address}${COL_NC} matches the IP found in ${PIHOLE_SETUP_VARS_FILE}" + else + # otherwise show it in red + log_write " ${COL_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_RED}Cannot reach the Internet.${COL_NC}\n" + return 1 + else + # Otherwise, show success + log_write "${TICK} ${COL_LIGHT_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_LIGHT_GREEN}${port_number}${COL_NC}] is in use by ${COL_LIGHT_GREEN}${service_name}${COL_NC}" + # Otherwise, + else + # Show the service name in red since it's non-standard + log_write "[${COL_LIGHT_RED}${port_number}${COL_NC}] is in use by ${COL_LIGHT_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_LIGHT_GREEN}${block_page}${COL_NC}" + else + # Otherwise, show an error + log_write "$CROSS ${COL_LIGHT_RED}X-Header does not match or could not be retrieved.${COL_NC}" + log_write "${COL_LIGHT_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_LIGHT_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_LIGHT_RED}X-Header does not match or could not be retrieved.${COL_NC}" + log_write "${COL_LIGHT_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_LIGHT_GREEN}is ${local_dig}${COL_NC} via ${COL_CYAN}localhost$COL_NC (${local_address})" + else + # Otherwise, show a failure + log_write "${CROSS} ${COL_LIGHT_RED}Failed to resolve${COL_NC} ${random_url} via ${COL_LIGHT_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_LIGHT_GREEN}is ${pihole_dig}${COL_NC} via ${COL_CYAN}Pi-hole${COL_NC} (${pihole_address})" + else + # Othewise, show a failure + log_write "${CROSS} ${COL_LIGHT_RED}Failed to resolve${COL_NC} ${random_url} via ${COL_LIGHT_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_LIGHT_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_LIGHT_RED}Failed to resolve${COL_NC} ${remote_url} via ${COL_LIGHT_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_LIGHT_GREEN}${i}${COL_NC} daemon is ${COL_LIGHT_GREEN}${status_of_process}${COL_NC}" + else + # If it's not, show it in red + log_write "${CROSS} ${COL_LIGHT_RED}${i}${COL_NC} daemon is ${COL_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_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_LIGHT_PURPLE}********************************************${COL_NC}" + log_write "${COL_LIGHT_PURPLE}********************************************${COL_NC}" + log_write "${TICK} ${COL_LIGHT_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_LIGHT_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_LIGHT_PURPLE}***********************************${COL_NC}" + log_write "${COL_LIGHT_PURPLE}***********************************${COL_NC}" + log_write "${TICK} Your debug token is: ${COL_LIGHT_GREEN}${tricorder_token}${COL_NC}" + log_write "${COL_LIGHT_PURPLE}***********************************${COL_NC}" + log_write "${COL_LIGHT_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_LIGHT_RED}48 hours${COL_NC}." + # If no token was generated + else + # Show an error and some help instructions + log_write "${CROSS} ${COL_LIGHT_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!" - -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 +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 fd66b255..2187f3ac 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -8,17 +8,41 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. -echo -n "::: Flushing /var/log/pihole.log ..." -# Test if logrotate is available on this system -if command -v /usr/sbin/logrotate &> /dev/null; then - # Flush twice to move all data out of sight of FTL - /usr/sbin/logrotate --force /etc/pihole/logrotate - /usr/sbin/logrotate --force /etc/pihole/logrotate +colfile="/opt/pihole/COL_TABLE" +source ${colfile} + +if [[ "$@" != *"quiet"* ]]; then + echo -ne " ${INFO} Flushing /var/log/pihole.log ..." +fi +if [[ "$@" == *"once"* ]]; then + # Nightly logrotation + if command -v /usr/sbin/logrotate >/dev/null; then + # Logrotate once + /usr/sbin/logrotate --force /etc/pihole/logrotate + else + # Copy pihole.log over to pihole.log.1 + # and empty out pihole.log + # Note that moving the file is not an option, as + # dnsmasq would happily continue writing into the + # moved file (it will have the same file handler) + cp /var/log/pihole.log /var/log/pihole.log.1 + echo " " > /var/log/pihole.log + fi else - # Flush both pihole.log and pihole.log.1 (if existing) - echo " " > /var/log/pihole.log - if [ -f /var/log/pihole.log.1 ]; then - echo " " > /var/log/pihole.log.1 + # Manual flushing + if command -v /usr/sbin/logrotate >/dev/null; then + # Logrotate twice to move all data out of sight of FTL + /usr/sbin/logrotate --force /etc/pihole/logrotate; sleep 3 + /usr/sbin/logrotate --force /etc/pihole/logrotate + else + # Flush both pihole.log and pihole.log.1 (if existing) + echo " " > /var/log/pihole.log + if [ -f /var/log/pihole.log.1 ]; then + echo " " > /var/log/pihole.log.1 + fi fi fi -echo "... done!" + +if [[ "$@" != *"quiet"* ]]; then + echo -e "${OVER} ${TICK} Flushed /var/log/pihole.log" +fi diff --git a/advanced/Scripts/update.sh b/advanced/Scripts/update.sh index 4fceb931..a4ada4c8 100755 --- a/advanced/Scripts/update.sh +++ b/advanced/Scripts/update.sh @@ -10,17 +10,19 @@ # 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" readonly PI_HOLE_FILES_DIR="/etc/.pihole" +# shellcheck disable=SC2034 PH_TEST=true -source ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh + +# 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 @@ -30,14 +32,14 @@ source ${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh GitCheckUpdateAvail() { local directory="${1}" curdir=$PWD - cd "${directory}" + cd "${directory}" || return # Fetch latest changes in this repo git fetch --quiet origin # @ alone is a shortcut for HEAD. Older versions of git # need @{0} - LOCAL="$(git rev-parse @{0})" + LOCAL="$(git rev-parse "@{0}")" # The suffix @{upstream} to a branchname # (short form @{u}) refers @@ -46,23 +48,23 @@ GitCheckUpdateAvail() { # (configured with branch..remote and # branch..merge). A missing branchname # defaults to the current one. - REMOTE="$(git rev-parse @{upstream})" + 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 # Change back to original directory - cd "${curdir}" + cd "${curdir}" || exit if [[ "${LOCAL}" != "${REMOTE}" ]]; then # Local branch is behind remote branch -> Update @@ -76,9 +78,10 @@ GitCheckUpdateAvail() { } FTLcheckUpdate() { - - local FTLversion=$(/usr/bin/pihole-FTL tag) - local FTLlatesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep 'Location' | awk -F '/' '{print $NF}' | tr -d '\r\n') + local FTLversion + FTLversion=$(/usr/bin/pihole-FTL tag) + local FTLlatesttag + FTLlatesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep 'Location' | awk -F '/' '{print $NF}' | tr -d '\r\n') if [[ "${FTLversion}" != "${FTLlatesttag}" ]]; then return 0 @@ -90,53 +93,59 @@ FTLcheckUpdate() { main() { local pihole_version_current local web_version_current + 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 - if ${FTL_update}; then - echo ":::" - echo "::: FTL out of date" + # 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 -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 @@ -151,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}" - 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}" - 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 + if [[ "${FTL_update}" == true ]]; then FTL_version_current="$(/usr/bin/pihole-FTL tag)" - echo ":::" - echo "::: FTL version is now at ${FTL_version_current}" + 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/version.sh b/advanced/Scripts/version.sh index 7f96e29a..f5e0f51d 100755 --- a/advanced/Scripts/version.sh +++ b/advanced/Scripts/version.sh @@ -3,24 +3,29 @@ # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # -# shows version numbers +# Show version numbers # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. # Variables DEFAULT="-1" -PHGITDIR="/etc/.pihole/" +COREGITDIR="/etc/.pihole/" WEBGITDIR="/var/www/html/admin/" getLocalVersion() { + # FTL requires a different method + if [[ "$1" == "FTL" ]]; then + pihole-FTL version + return 0 + fi + # Get the tagged version of the local repository local directory="${1}" local version - cd "${directory}" || { echo "${DEFAULT}"; return 1; } - version=$(git describe --tags --always || \ - echo "${DEFAULT}") + cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; } + version=$(git describe --tags --always || echo "$DEFAULT") if [[ "${version}" =~ ^v ]]; then echo "${version}" elif [[ "${version}" == "${DEFAULT}" ]]; then @@ -33,13 +38,18 @@ getLocalVersion() { } getLocalHash() { + # Local FTL hash does not exist on filesystem + if [[ "$1" == "FTL" ]]; then + echo "N/A" + return 0 + fi + # Get the short hash of the local repository local directory="${1}" local hash - cd "${directory}" || { echo "${DEFAULT}"; return 1; } - hash=$(git rev-parse --short HEAD || \ - echo "${DEFAULT}") + cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; } + hash=$(git rev-parse --short HEAD || echo "$DEFAULT") if [[ "${hash}" == "${DEFAULT}" ]]; then echo "ERROR" return 1 @@ -49,12 +59,33 @@ getLocalHash() { return 0 } +getRemoteHash(){ + # Remote FTL hash is not applicable + if [[ "$1" == "FTL" ]]; then + echo "N/A" + return 0 + fi + + local daemon="${1}" + local branch="${2}" + + hash=$(git ls-remote --heads "https://github.com/pi-hole/${daemon}" | \ + awk -v bra="$branch" '$0~bra {print substr($0,0,8);exit}') + if [[ -n "$hash" ]]; then + echo "$hash" + else + echo "ERROR" + return 1 + fi + return 0 +} + getRemoteVersion(){ # Get the version from the remote origin local daemon="${1}" local version - version=$(curl --silent --fail https://api.github.com/repos/pi-hole/${daemon}/releases/latest | \ + version=$(curl --silent --fail "https://api.github.com/repos/pi-hole/${daemon}/releases/latest" | \ awk -F: '$1 ~/tag_name/ { print $2 }' | \ tr -cd '[[:alnum:]]._-') if [[ "${version}" =~ ^v ]]; then @@ -66,72 +97,72 @@ getRemoteVersion(){ return 0 } -#PHHASHLATEST=$(curl -s https://api.github.com/repos/pi-hole/pi-hole/commits/master | \ -# grep sha | \ -# head -n1 | \ -# awk -F ' ' '{ print $2 }' | \ -# tr -cd '[[:alnum:]]._-') - -#WEBHASHLATEST=$(curl -s https://api.github.com/repos/pi-hole/AdminLTE/commits/master | \ -# grep sha | \ -# head -n1 | \ -# awk -F ' ' '{ print $2 }' | \ -# tr -cd '[[:alnum:]]._-') - - -normalOutput() { - echo "::: Pi-hole version is $(getLocalVersion "${PHGITDIR}") (Latest version is $(getRemoteVersion pi-hole))" - if [ -d "${WEBGITDIR}" ]; then - echo "::: Web-Admin version is $(getLocalVersion "${WEBGITDIR}") (Latest version is $(getRemoteVersion AdminLTE))" - fi -} - -webOutput() { - if [ -d "${WEBGITDIR}" ]; then - case "${1}" in - "-l" | "--latest" ) echo $(getRemoteVersion AdminLTE);; - "-c" | "--current" ) echo $(getLocalVersion "${WEBGITDIR}");; - "-h" | "--hash" ) echo $(getLocalHash "${WEBGITDIR}");; - * ) echo "::: Invalid Option!"; exit 1; - esac - else - echo "::: Web interface not installed!"; exit 1; +versionOutput() { + [[ "$1" == "pi-hole" ]] && GITDIR=$COREGITDIR + [[ "$1" == "AdminLTE" ]] && GITDIR=$WEBGITDIR + [[ "$1" == "FTL" ]] && GITDIR="FTL" + + [[ "$2" == "-c" ]] || [[ "$2" == "--current" ]] || [[ -z "$2" ]] && current=$(getLocalVersion $GITDIR) + [[ "$2" == "-l" ]] || [[ "$2" == "--latest" ]] || [[ -z "$2" ]] && latest=$(getRemoteVersion "$1") + if [[ "$2" == "-h" ]] || [[ "$2" == "--hash" ]]; then + [[ "$3" == "-c" ]] || [[ "$3" == "--current" ]] || [[ -z "$3" ]] && curHash=$(getLocalHash "$GITDIR") + [[ "$3" == "-l" ]] || [[ "$3" == "--latest" ]] || [[ -z "$3" ]] && latHash=$(getRemoteHash "$1" "$(cd "$GITDIR" 2> /dev/null && git rev-parse --abbrev-ref HEAD)") fi + + if [[ -n "$current" ]] && [[ -n "$latest" ]]; then + output="${1^} version is $current (Latest: $latest)" + elif [[ -n "$current" ]] && [[ -z "$latest" ]]; then + output="Current ${1^} version is $current" + elif [[ -z "$current" ]] && [[ -n "$latest" ]]; then + output="Latest ${1^} version is $latest" + elif [[ "$curHash" == "N/A" ]] || [[ "$latHash" == "N/A" ]]; then + output="${1^} hash is not applicable" + elif [[ -n "$curHash" ]] && [[ -n "$latHash" ]]; then + output="${1^} hash is $curHash (Latest: $latHash)" + elif [[ -n "$curHash" ]] && [[ -z "$latHash" ]]; then + output="Current ${1^} hash is $curHash" + elif [[ -z "$curHash" ]] && [[ -n "$latHash" ]]; then + output="Latest ${1^} hash is $latHash" + else + errorOutput + fi + + [[ -n "$output" ]] && echo " $output" } -coreOutput() { - case "${1}" in - "-l" | "--latest" ) echo $(getRemoteVersion pi-hole);; - "-c" | "--current" ) echo $(getLocalVersion "${PHGITDIR}");; - "-h" | "--hash" ) echo $(getLocalHash "${PHGITDIR}");; - * ) echo "::: Invalid Option!"; exit 1; - esac +errorOutput() { + echo " Invalid Option! Try 'pihole -v --help' for more information." + exit 1 +} + +defaultOutput() { + versionOutput "pi-hole" "$@" + versionOutput "AdminLTE" "$@" + versionOutput "FTL" "$@" } helpFunc() { - cat << EOM -::: -::: Show Pi-hole/Web Admin versions -::: -::: Usage: pihole -v [ -a | -p ] [ -l | -c ] -::: -::: Options: -::: -a, --admin Show both current and latest versions of web admin -::: -p, --pihole Show both current and latest versions of Pi-hole core files -::: -l, --latest (Only after -a | -p) Return only latest version -::: -c, --current (Only after -a | -p) Return only current version -::: -h, --help Show this help dialog -::: -EOM - exit 0 + echo "Usage: pihole -v [repo | option] [option] +Example: 'pihole -v -p -l' +Show Pi-hole, Admin Console & FTL versions + +Repositories: + -p, --pihole Only retrieve info regarding Pi-hole repository + -a, --admin Only retrieve info regarding AdminLTE repository + -f, --ftl Only retrieve info regarding FTL repository + +Options: + -c, --current Return the current version + -l, --latest Return the latest version + --hash Return the Github hash from your local repositories + -h, --help Show this help dialog" + exit 0 } -if [[ $# = 0 ]]; then - normalOutput -fi - case "${1}" in - "-a" | "--admin" ) shift; webOutput "$@";; - "-p" | "--pihole" ) shift; coreOutput "$@" ;; + "-p" | "--pihole" ) shift; versionOutput "pi-hole" "$@";; + "-a" | "--admin" ) shift; versionOutput "AdminLTE" "$@";; + "-f" | "--ftl" ) shift; versionOutput "FTL" "$@";; "-h" | "--help" ) helpFunc;; + * ) defaultOutput "$@";; esac diff --git a/advanced/Scripts/webpage.sh b/advanced/Scripts/webpage.sh index 7804fc8f..9d101482 100755 --- a/advanced/Scripts/webpage.sh +++ b/advanced/Scripts/webpage.sh @@ -8,31 +8,31 @@ # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. - readonly setupVars="/etc/pihole/setupVars.conf" readonly dnsmasqconfig="/etc/dnsmasq.d/01-pihole.conf" readonly dhcpconfig="/etc/dnsmasq.d/02-pihole-dhcp.conf" # 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() { - cat << EOM -::: Set admin options for the web interface of pihole -::: -::: Usage: pihole -a [options] -::: -::: Options: -::: -p, password Set web interface password, an empty input will remove any previously set password -::: -c, celsius Set Celsius temperature unit -::: -f, fahrenheit Set Fahrenheit temperature unit -::: -k, kelvin Set Kelvin temperature unit -::: -h, --help Show this help dialog -::: -i, interface Setup interface listening behavior of dnsmasq -::: pihole -a -i local : Listen on all interfaces, but allow only queries from -::: devices that are at most one hop away (local devices) -::: pihole -a -i single : Listen only on one interface (see PIHOLE_INTERFACE) -::: pihole -a -i all : Listen on all interfaces, permit all origins -EOM + echo "Usage: pihole -a [options] +Example: pihole -a -p password +Set options for the Admin Console + +Options: + -p, password Set Admin Console password + -c, celsius Set Celsius as preferred temperature unit + -f, fahrenheit Set Fahrenheit as preferred temperature unit + -k, kelvin Set Kelvin as preferred temperature unit + -r, hostrecord Add a name to the DNS associated to an IPv4/IPv6 address + -h, --help Show this help dialog + -i, interface Specify dnsmasq's interface listening behavior + Add '-h' for more info on interface usage" exit 0 } @@ -61,14 +61,19 @@ delete_dnsmasq_setting() { sed -i "/${1}/d" "${dnsmasqconfig}" } -SetTemperatureUnit(){ - +SetTemperatureUnit() { change_setting "TEMPERATUREUNIT" "${unit}" - + echo -e " ${TICK} Set temperature unit to ${unit}" } -SetWebPassword(){ +HashPassword() { + # Compute password hash twice to avoid rainbow table vulnerability + return=$(echo -n ${1} | sha256sum | sed 's/\s.*$//') + return=$(echo -n ${return} | sha256sum | sed 's/\s.*$//') + echo ${return} +} +SetWebPassword() { if [ "${SUDO_USER}" == "www-data" ]; then echo "Security measure: user www-data is not allowed to change webUI password!" echo "Exiting" @@ -81,26 +86,33 @@ SetWebPassword(){ exit 1 fi - read -s -p "Enter New Password (Blank for no password): " PASSWORD - echo "" + if (( ${#args[2]} > 0 )) ; then + 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" - exit 0 - fi + if [ "${PASSWORD}" == "" ]; then + change_setting "WEBPASSWORD" "" + echo -e " ${TICK} Password Removed" + exit 0 + fi + + read -s -p "Confirm Password: " CONFIRM + echo "" + fi - read -s -p "Confirm Password: " CONFIRM - echo "" if [ "${PASSWORD}" == "${CONFIRM}" ] ; then - # Compute password hash twice to avoid rainbow table vulnerability - hash=$(echo -n ${PASSWORD} | sha256sum | sed 's/\s.*$//') - hash=$(echo -n ${hash} | sha256sum | sed 's/\s.*$//') + 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 } @@ -164,8 +176,7 @@ trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE3 } -SetDNSServers(){ - +SetDNSServers() { # Save setting to file delete_setting "PIHOLE_DNS" IFS=',' read -r -a array <<< "${args[2]}" @@ -196,72 +207,68 @@ SetDNSServers(){ # Restart dnsmasq to load new configuration RestartDNS - } -SetExcludeDomains(){ - +SetExcludeDomains() { change_setting "API_EXCLUDE_DOMAINS" "${args[2]}" - } -SetExcludeClients(){ - +SetExcludeClients() { change_setting "API_EXCLUDE_CLIENTS" "${args[2]}" - } -Reboot(){ - +Reboot() { nohup bash -c "sleep 5; reboot" &> /dev/null /dev/null - else - service dnsmasq restart &> /dev/null - fi +RestartDNS() { + local str="Restarting DNS service" + [[ -t 1 ]] && echo -ne " ${INFO} ${str}" + if command -v systemctl &> /dev/null; then + output=$( { systemctl restart dnsmasq; } 2>&1 ) + else + output=$( { service dnsmasq restart; } 2>&1 ) + fi + if [[ -z "${output}" ]]; then + [[ -t 1 ]] && echo -e "${OVER} ${TICK} ${str}" + else + [[ ! -t 1 ]] && OVER="" + echo -e "${OVER} ${CROSS} ${output}" + fi } -SetQueryLogOptions(){ - +SetQueryLogOptions() { change_setting "API_QUERY_LOG_SHOW" "${args[2]}" - } ProcessDHCPSettings() { - source "${setupVars}" if [[ "${DHCP_ACTIVE}" == "true" ]]; then + interface=$(grep 'PIHOLE_INTERFACE=' /etc/pihole/setupVars.conf | sed "s/.*=//") - interface=$(grep 'PIHOLE_INTERFACE=' /etc/pihole/setupVars.conf | sed "s/.*=//") + # Use eth0 as fallback interface + if [ -z ${interface} ]; then + interface="eth0" + fi - # Use eth0 as fallback interface - if [ -z ${interface} ]; then - interface="eth0" - fi + if [[ "${PIHOLE_DOMAIN}" == "" ]]; then + PIHOLE_DOMAIN="local" + change_setting "PIHOLE_DOMAIN" "${PIHOLE_DOMAIN}" + fi - if [[ "${PIHOLE_DOMAIN}" == "" ]]; then - PIHOLE_DOMAIN="local" - change_setting "PIHOLE_DOMAIN" "${PIHOLE_DOMAIN}" - fi + if [[ "${DHCP_LEASETIME}" == "0" ]]; then + leasetime="infinite" + elif [[ "${DHCP_LEASETIME}" == "" ]]; then + leasetime="24h" + change_setting "DHCP_LEASETIME" "${leasetime}" + else + leasetime="${DHCP_LEASETIME}h" + fi - if [[ "${DHCP_LEASETIME}" == "0" ]]; then - leasetime="infinite" - elif [[ "${DHCP_LEASETIME}" == "" ]]; then - leasetime="24h" - change_setting "DHCP_LEASETIME" "${leasetime}" - else - leasetime="${DHCP_LEASETIME}h" - fi - - # Write settings to file - echo "############################################################################### + # Write settings to file + echo "############################################################################### # DHCP SERVER CONFIG FILE AUTOMATICALLY POPULATED BY PI-HOLE WEB INTERFACE. # # ANY CHANGES MADE TO THIS FILE WILL BE LOST ON CHANGE # ############################################################################### @@ -272,26 +279,27 @@ dhcp-leasefile=/etc/pihole/dhcp.leases #quiet-dhcp " > "${dhcpconfig}" -if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then - echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" -fi + if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then + echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" + fi - if [[ "${DHCP_IPv6}" == "true" ]]; then -echo "#quiet-dhcp6 + if [[ "${DHCP_IPv6}" == "true" ]]; then + echo "#quiet-dhcp6 #enable-ra dhcp-option=option6:dns-server,[::] dhcp-range=::100,::1ff,constructor:${interface},ra-names,slaac,${leasetime} ra-param=*,0,0 " >> "${dhcpconfig}" - fi + fi else - rm "${dhcpconfig}" &> /dev/null + if [[ -f "${dhcpconfig}" ]]; then + rm "${dhcpconfig}" &> /dev/null + fi fi } -EnableDHCP(){ - +EnableDHCP() { change_setting "DHCP_ACTIVE" "true" change_setting "DHCP_START" "${args[2]}" change_setting "DHCP_END" "${args[3]}" @@ -309,8 +317,7 @@ EnableDHCP(){ RestartDNS } -DisableDHCP(){ - +DisableDHCP() { change_setting "DHCP_ACTIVE" "false" # Remove possible old setting from file @@ -322,23 +329,20 @@ DisableDHCP(){ RestartDNS } -SetWebUILayout(){ - +SetWebUILayout() { change_setting "WEBUIBOXEDLAYOUT" "${args[2]}" - } CustomizeAdLists() { - list="/etc/pihole/adlists.list" - if [[ "${args[2]}" == "enable" ]] ; then + if [[ "${args[2]}" == "enable" ]]; then sed -i "\\@${args[3]}@s/^#http/http/g" "${list}" - elif [[ "${args[2]}" == "disable" ]] ; then + elif [[ "${args[2]}" == "disable" ]]; then sed -i "\\@${args[3]}@s/^http/#http/g" "${list}" - elif [[ "${args[2]}" == "add" ]] ; then + elif [[ "${args[2]}" == "add" ]]; then echo "${args[3]}" >> ${list} - elif [[ "${args[2]}" == "del" ]] ; then + elif [[ "${args[2]}" == "del" ]]; then var=$(echo "${args[3]}" | sed 's/\//\\\//g') sed -i "/${var}/Id" "${list}" else @@ -347,18 +351,15 @@ CustomizeAdLists() { fi } -SetPrivacyMode(){ - - if [[ "${args[2]}" == "true" ]] ; then +SetPrivacyMode() { + if [[ "${args[2]}" == "true" ]]; then change_setting "API_PRIVACY_MODE" "true" else change_setting "API_PRIVACY_MODE" "false" fi - } ResolutionSettings() { - typ="${args[2]}" state="${args[3]}" @@ -367,11 +368,9 @@ ResolutionSettings() { elif [[ "${typ}" == "clients" ]]; then change_setting "API_GET_CLIENT_HOSTNAME" "${state}" fi - } AddDHCPStaticAddress() { - mac="${args[2]}" ip="${args[3]}" host="${args[4]}" @@ -386,45 +385,63 @@ AddDHCPStaticAddress() { # Full info given echo "dhcp-host=${mac},${ip},${host}" >> "${dhcpstaticconfig}" fi - } RemoveDHCPStaticAddress() { - mac="${args[2]}" sed -i "/dhcp-host=${mac}.*/d" "${dhcpstaticconfig}" - } -SetHostRecord(){ +SetHostRecord() { + if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then + echo "Usage: pihole -a hostrecord [IPv4-address],[IPv6-address] +Example: 'pihole -a hostrecord home.domain.com 192.168.1.1,2001:db8:a0b:12f0::1' +Add a name to the DNS associated to an IPv4/IPv6 address - if [ -n "${args[3]}" ]; then +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 # Restart dnsmasq to load new configuration RestartDNS - } -SetListeningMode(){ - +SetListeningMode() { source "${setupVars}" - if [[ "${args[2]}" == "all" ]] ; then - echo "Listening on all interfaces, permiting all origins, hope you have a firewall!" + if [[ "$3" == "-h" ]] || [[ "$3" == "--help" ]]; then + echo "Usage: pihole -a -i [interface] +Example: 'pihole -a -i local' +Specify dnsmasq's network interface listening behavior + +Interfaces: + local Listen on all interfaces, but only allow queries from + devices that are at most one hop away (local devices) + single Listen only on ${PIHOLE_INTERFACE} interface + all Listen on all interfaces, permit all origins" + exit 0 + fi + + if [[ "${args[2]}" == "all" ]]; then + 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)" + elif [[ "${args[2]}" == "local" ]]; then + 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 @@ -435,17 +452,19 @@ SetListeningMode(){ # Restart dnsmasq to load new configuration RestartDNS fi - } -Teleporter() -{ +Teleporter() { local datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S") php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "pi-hole-teleporter_${datetimestamp}.zip" } -main() { +audit() +{ + echo "${args[2]}" >> /etc/pihole/auditlog.list +} +main() { args=("$@") case "${args[1]}" in @@ -467,10 +486,11 @@ main() { "resolve" ) ResolutionSettings;; "addstaticdhcp" ) AddDHCPStaticAddress;; "removestaticdhcp" ) RemoveDHCPStaticAddress;; - "hostrecord" ) SetHostRecord;; - "-i" | "interface" ) SetListeningMode;; + "-r" | "hostrecord" ) SetHostRecord "$3";; + "-i" | "interface" ) SetListeningMode "$@";; "-t" | "teleporter" ) Teleporter;; "adlist" ) CustomizeAdLists;; + "audit" ) audit;; * ) helpFunc;; esac @@ -479,5 +499,4 @@ main() { if [[ $# = 0 ]]; then helpFunc fi - } diff --git a/advanced/index.php b/advanced/index.php index bfc44a1d..1dd5acc7 100644 --- a/advanced/index.php +++ b/advanced/index.php @@ -185,7 +185,7 @@ function add() { } $.ajax({ - url: "admin/scripts/pi-hole/php/add.php", + url: "/admin/scripts/pi-hole/php/add.php", method: "post", data: {"domain":domain.val(), "list":"white", "pw":pw.val()}, success: function(response) { diff --git a/advanced/logrotate b/advanced/logrotate index 570e7548..ffed910b 100644 --- a/advanced/logrotate +++ b/advanced/logrotate @@ -8,3 +8,14 @@ notifempty nomail } + +/var/log/pihole-FTL.log { + # su # + weekly + copytruncate + rotate 3 + compress + delaycompress + notifempty + nomail +} diff --git a/advanced/pihole-FTL.service b/advanced/pihole-FTL.service index 30cd140f..627fad8c 100644 --- a/advanced/pihole-FTL.service +++ b/advanced/pihole-FTL.service @@ -26,7 +26,7 @@ start() { echo "pihole-FTL is already running" else touch /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port - chown pihole:pihole /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port + chown pihole:pihole /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /etc/pihole chmod 0644 /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port su -s /bin/sh -c "/usr/bin/pihole-FTL" "$FTLUSER" echo diff --git a/advanced/pihole.cron b/advanced/pihole.cron index c885b371..f1beb08c 100644 --- a/advanced/pihole.cron +++ b/advanced/pihole.cron @@ -21,9 +21,10 @@ # Pi-hole: Update Pi-hole! Uncomment to enable auto update #30 2 * * 7 root PATH="$PATH:/usr/local/bin/" pihole updatePihole -# Pi-hole: Flush the log daily at 00:00 so it doesn't get out of control -# Stats will be viewable in the Web interface thanks to the cron job above +# Pi-hole: Flush the log daily at 00:00 # The flush script will use logrotate if available -00 00 * * * root PATH="$PATH:/usr/local/bin/" pihole flush +# parameter "once": logrotate only once (default is twice) +# parameter "quiet": don't print messages +00 00 * * * root PATH="$PATH:/usr/local/bin/" pihole flush once quiet @reboot root /usr/sbin/logrotate /etc/pihole/logrotate diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index d7075088..4047502b 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -1,38 +1,60 @@ #!/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 @@ -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,48 +119,69 @@ 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" - UPDATE_PKG_CACHE="test_dpkg_lock; ${PKG_MANAGER} update" + # 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 - # ######################################### + + # 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) + # 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) + # 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) + # 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" - test_dpkg_lock() { +# A function to check... +test_dpkg_lock() { + # An iterator used for counting loop iterations i=0 + # fuser is a program to show which processes use the named files, sockets, or filesystems + # So while the command is true while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do + # Wait half a second sleep 0.5 + # and increase the iterator ((i=i+1)) done # Always return success, since we only return if there is no @@ -124,15 +189,16 @@ if command -v apt-get &> /dev/null; then 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" @@ -147,139 +213,233 @@ elif command -v rpm &> /dev/null; then LIGHTTPD_CFG="lighttpd.conf.fedora" DNSMASQ_USER="nobody" +# If neither apt-get or rmp/dnf are not 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 + # 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 $? + # 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}