Merge pull request #2308 from pi-hole/release/v4.0

Release/v4.0
pull/2353/head v4.0
Dan Schaper 6 years ago committed by GitHub
commit ddbdb51d20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,7 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
indent_size = tab indent_size = tab
tab_width = 2 tab_width = 4
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

2
.github/dco.yml vendored

@ -0,0 +1,2 @@
require:
members: false

5
.gitignore vendored

@ -3,6 +3,11 @@
*.swp *.swp
__pycache__ __pycache__
.cache .cache
.pytest_cache
.tox
.eggs
*.egg-info
# Created by https://www.gitignore.io/api/jetbrains+iml # Created by https://www.gitignore.io/api/jetbrains+iml

@ -1,11 +1,5 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</value>
</option>
<MarkdownNavigatorCodeStyleSettings> <MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" /> <option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings> </MarkdownNavigatorCodeStyleSettings>

@ -1,3 +1,6 @@
linters: linters:
shellcheck: shellcheck:
shell: bash shell: bash
phpcs:
csslint:
flake8:

@ -7,4 +7,6 @@ python:
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
script: py.test -vv script:
# tox.ini handles setup, ordering of docker build first, and then run tests
- tox

@ -1,5 +1,3 @@
_This template was created based on the work of [`udemy-dl`](https://github.com/nishad/udemy-dl/blob/master/LICENSE)._
# Contributors Guide # Contributors Guide
Please read and understand the contribution guide before creating an issue or pull request. Please read and understand the contribution guide before creating an issue or pull request.

@ -3,7 +3,7 @@
<b>Network-wide ad blocking via your own Linux hardware</b><br/> <b>Network-wide ad blocking via your own Linux hardware</b><br/>
</p> </p>
The Pi-hole is a [DNS sinkhole](https://en.wikipedia.org/wiki/DNS_Sinkhole) that protects your devices from unwanted content, without installing any client-side software. The Pi-hole[®](https://pi-hole.net/trademark-rules-and-brand-guidelines/) is a [DNS sinkhole](https://en.wikipedia.org/wiki/DNS_Sinkhole) that protects your devices from unwanted content, without installing any client-side software.
- **Easy-to-install**: our versatile installer walks you through the process, and [takes less than ten minutes](https://www.youtube.com/watch?v=vKWjx1AQYgs) - **Easy-to-install**: our versatile installer walks you through the process, and [takes less than ten minutes](https://www.youtube.com/watch?v=vKWjx1AQYgs)
- **Resolute**: content is blocked in _non-browser locations_, such as ad-laden mobile apps and smart TVs - **Resolute**: content is blocked in _non-browser locations_, such as ad-laden mobile apps and smart TVs
@ -27,7 +27,7 @@ Those who want to get started quickly and conveniently, may install Pi-hole usin
#### `curl -sSL https://install.pi-hole.net | bash` #### `curl -sSL https://install.pi-hole.net | bash`
## Alternative Install Methods ## Alternative Install Methods
[Piping to `bash` is controversial](https://pi-hole.net/2016/07/25/curling-and-piping-to-bash), as it prevents you from [reading code that is about to run](https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh) on your system. Therefore, we provide these alternative installation methods which allow code review before installation: [Piping to `bash` is controversial](https://pi-hole.net/2016/07/25/curling-and-piping-to-bash), as it prevents you from [reading code that is about to run](https://github.com/pi-hole/pi-hole/blob/master/automated%20install/basic-install.sh) on your system. Therefore, we provide these alternative installation methods which allow code review before installation:
### Method 1: Clone our repository and run ### Method 1: Clone our repository and run
``` ```
@ -60,16 +60,21 @@ Make no mistake: **your support is absolutely vital to help keep us innovating!*
### Donations ### Donations
Sending a donation using our links below is **extremely helpful** in offsetting a portion of our monthly expenses: Sending a donation using our links below is **extremely helpful** in offsetting a portion of our monthly expenses:
&nbsp;<img src="https://pi-hole.github.io/graphics/Badges/paypal-badge-black.svg" width="24" height="24" alt="PP"/> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3J2L3Z4DHW9UY">Donate via PayPal</a><br/> - <img src="https://pi-hole.github.io/graphics/Badges/paypal-badge-black.svg" width="24" height="24" alt="PP"/> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=3J2L3Z4DHW9UY">Donate via PayPal</a><br/>
&nbsp;<img src="https://pi-hole.github.io/graphics/Badges/bitcoin-badge-black.svg" width="24" height="24" alt="BTC"/> Bitcoin Address: <code>1GKnevUnVaQM2pQieMyeHkpr8DXfkpfAtL</code> - <img src="https://pi-hole.github.io/graphics/Badges/bitcoin-badge-black.svg" width="24" height="24" alt="BTC"/> [Bitcoin](https://commerce.coinbase.com/checkout/fb7facaf-bebd-46be-bb77-b358f4546763): <code>
3MDPzjXu2hjw5sGLJvKUi1uXbvQPzVrbpF</code></br>
- <img src="https://pi-hole.github.io/graphics/Badges/bitcoin-badge-black.svg" width="24" height="24" alt="BTC"/> [Bitcoin Cash](https://commerce.coinbase.com/checkout/fb7facaf-bebd-46be-bb77-b358f4546763): <code>qzqsz4aju2eecc6uhs7tus4vlwhhela24sdruf4qp5</code></br>
- <img src="https://pi-hole.github.io/graphics/Badges/ethereum-badge-black.svg" width="24" height="24" alt="BTC"/> [Ethereum](https://commerce.coinbase.com/checkout/fb7facaf-bebd-46be-bb77-b358f4546763): <code>0x79d4e90A4a0C732819526c93e21A3F1356A2FAe1</code>
### Alternative support ### Alternative support
If you'd rather not donate (_which is okay!_), there are other ways you can help support us: If you'd rather not [donate](https://pi-hole.net/donate/) (_which is okay!_), there are other ways you can help support us:
- [Patreon](https://patreon.com/pihole) _Become a patron for rewards_
- [Digital Ocean](http://www.digitalocean.com/?refcode=344d234950e1) affiliate link - [Digital Ocean](http://www.digitalocean.com/?refcode=344d234950e1) _affiliate link_
- [Vultr](http://www.vultr.com/?ref=7190426) affiliate link - [UNIXstickers.com](http://unixstickers.refr.cc/jacobs) _save $5 when you spend $9 using our affiliate link_
- [UNIXstickers.com](http://unixstickers.refr.cc/jacobs) affiliate link - [Pi-hole Swag Store](https://pi-hole.net/shop/) _affiliate link_
- [Pi-hole Swag Store](https://pi-hole.net/shop/) - [Amazon](http://www.amazon.com/exec/obidos/redirect-home/pihole09-20) _affiliate link_
- [DNS Made Easy](https://cp.dnsmadeeasy.com/u/133706) _affiliate link_
- [Vultr](http://www.vultr.com/?ref=7190426) _affiliate link_
- Spreading the word about our software, and how you have benefited from it - Spreading the word about our software, and how you have benefited from it
### Contributing via GitHub ### Contributing via GitHub
@ -93,9 +98,6 @@ While we are primarily reachable on our <a href="https://discourse.pi-hole.net/"
<li><a href="https://discourse.pi-hole.net/c/faqs">Frequently Asked Questions</a></li> <li><a href="https://discourse.pi-hole.net/c/faqs">Frequently Asked Questions</a></li>
<li><a href="https://github.com/pi-hole/pi-hole/wiki">Pi-hole Wiki</a></li> <li><a href="https://github.com/pi-hole/pi-hole/wiki">Pi-hole Wiki</a></li>
<li><a href="https://discourse.pi-hole.net/c/feature-requests?order=votes">Feature Requests</a></li> <li><a href="https://discourse.pi-hole.net/c/feature-requests?order=votes">Feature Requests</a></li>
</ul>
<br/>
<ul>
<li><a href="https://discourse.pi-hole.net/">Discourse User Forum</a></li> <li><a href="https://discourse.pi-hole.net/">Discourse User Forum</a></li>
<li><a href="https://www.reddit.com/r/pihole/">Reddit</a></li> <li><a href="https://www.reddit.com/r/pihole/">Reddit</a></li>
<li><a href="https://gitter.im/pi-hole/pi-hole">Gitter</a> (Real-time chat)</li> <li><a href="https://gitter.im/pi-hole/pi-hole">Gitter</a> (Real-time chat)</li>
@ -127,7 +129,7 @@ You can read our [Core Feature Breakdown](https://github.com/pi-hole/pi-hole/wik
### The Web Interface Dashboard ### The Web Interface Dashboard
This [optional dashboard](https://github.com/pi-hole/AdminLTE) allows you to view stats, change settings, and configure your Pi-hole. It's the power of the Command Line Interface, with none of the learning curve! This [optional dashboard](https://github.com/pi-hole/AdminLTE) allows you to view stats, change settings, and configure your Pi-hole. It's the power of the Command Line Interface, with none of the learning curve!
<a href="https://pi-hole.github.io/graphics/Screenshots/dashboard.png"><img src="https://pi-hole.github.io/graphics/Screenshots/dashboard.png" width="888" height="522" alt="Pi-hole Dashboard"/></a> <img src="https://pi-hole.github.io/graphics/Screenshots/pihole-dashboard.png" alt="Pi-hole Dashboard"/></a>
Some notable features include: Some notable features include:
* Mobile friendly interface * Mobile friendly interface
@ -142,11 +144,11 @@ Some notable features include:
There are several ways to [access the dashboard](https://discourse.pi-hole.net/t/how-do-i-access-pi-holes-dashboard-admin-interface/3168): 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://<IP_ADDPRESS_OF_YOUR_PI_HOLE>/admin/` 1. `http://<IP_ADDPRESS_OF_YOUR_PI_HOLE>/admin/`
2. `http:/pi.hole/admin/` (when using Pi-hole as your DNS server) 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) 3. `http://pi.hole/` (when using Pi-hole as your DNS server)
## The Faster-Than-Light Engine ## Faster-than-light Engine
The [FTL Engine](https://github.com/pi-hole/FTL) is a lightweight, purpose-built daemon used to provide statistics needed for the Web Interface, and its API can be easily integrated into your own projects. As the name implies, FTL does this all *very quickly*! FTLDNS[™](https://pi-hole.net/trademark-rules-and-brand-guidelines/) is a lightweight, purpose-built daemon used to provide statistics needed for the Web Interface, and its API can be easily integrated into your own projects. As the name implies, FTLDNS does this all *very quickly*!
Some of the statistics you can integrate include: Some of the statistics you can integrate include:
* Total number of domains being blocked * Total number of domains being blocked
@ -172,31 +174,13 @@ Pi-hole being a **advertising-aware DNS/Web server**, makes use of the following
* [AdminLTE Dashboard](https://github.com/almasaeed2010/AdminLTE) - premium admin control panel based on Bootstrap 3.x * [AdminLTE Dashboard](https://github.com/almasaeed2010/AdminLTE) - premium admin control panel based on Bootstrap 3.x
While quite outdated at this point, [this original blog post about Pi-hole](https://jacobsalmela.com/2015/06/16/block-millions-ads-network-wide-with-a-raspberry-pi-hole-2-0/) goes into **great detail** about how Pi-hole was originally setup and how it works. Syntactically, it's no longer accurate, but the same basic principles and logic still apply to Pi-hole's current state. While quite outdated at this point, [this original blog post about Pi-hole](https://jacobsalmela.com/2015/06/16/block-millions-ads-network-wide-with-a-raspberry-pi-hole-2-0/) goes into **great detail** about how Pi-hole was originally setup and how it works. Syntactically, it's no longer accurate, but the same basic principles and logic still apply to Pi-hole's current state.
-----
## Pi-hole Projects
- [The Big Blocklist Collection](https://wally3k.github.io)
- [Docker Pi-hole container (x86 and ARM)](https://hub.docker.com/r/diginc/pi-hole/)
- [Pi-Hole in the cloud](http://blog.codybunch.com/2015/07/28/Pi-Hole-in-the-cloud/)
- [Pie in the Sky-Hole [A Pi-Hole in the cloud for ad-blocking via DNS]](https://dlaa.me/blog/post/skyhole)
- [Pi-hole Enable/Disable Button](http://thetimmy.silvernight.org/pages/endisbutton/)
- [Minibian Pi-hole](https://munkjensen.net/wiki/index.php/See_my_Pi-Hole#Minibian_Pi-hole)
- [CHiP-hole: Network-wide Ad-blocker](https://www.hackster.io/jacobsalmela/chip-hole-network-wide-ad-blocker-98e037)
- [Chrome Extension: Pi-Hole List Editor](https://chrome.google.com/webstore/detail/pi-hole-list-editor/hlnoeoejkllgkjbnnnhfolapllcnaglh) ([Source Code](https://github.com/packtloss/pihole-extension))
- [Splunk: Pi-hole Visualiser](https://splunkbase.splunk.com/app/3023/)
- [Adblocking with Pi-hole and Ubuntu 14.04 on VirtualBox](https://hbalagtas.blogspot.com.au/2016/02/adblocking-with-pi-hole-and-ubuntu-1404.html)
- [Pi-hole stats in your Mac's menu bar](https://getbitbar.com/plugins/Network/pi-hole.1m.py)
- [Pi-hole unRAID Template](https://forums.lime-technology.com/topic/36810-support-spants-nodered-mqtt-dashing-couchdb/)
- [Copernicus: Windows Tray Application](https://github.com/goldbattle/copernicus)
- [Let your blink1 device blink when Pi-hole filters ads](https://gist.github.com/elpatron68/ec0b4c582e5abf604885ac1e068d233f)
- [Pi-hole metrics](https://github.com/nlamirault/pihole_exporter) exporter for [Prometheus](https://prometheus.io/)
- [Magic Mirror with DNS Filtering](https://zonksec.com/blog/magic-mirror-dns-filtering/#dnssoftware)
- [Pi-hole Droid: Android client](https://github.com/friimaind/pi-hole-droid)
- [Windows DNS Swapper](https://github.com/roots84/DNS-Swapper), see [#1400](https://github.com/pi-hole/pi-hole/issues/1400)
----- -----
## Coverage ## Coverage
- [Software Engineering Daily: Interview with the creator of Pi-hole](https://softwareengineeringdaily.com/2018/05/29/pi-hole-ad-blocker-hardware-with-jacob-salmela/)
- [Bloomberg Business Week: Brotherhood of the Ad blockers](https://www.bloomberg.com/news/features/2018-05-10/inside-the-brotherhood-of-pi-hole-ad-blockers)
- [Securing DNS across all of my devices with Pi-Hole + DNS-over-HTTPS + 1.1.1.1](https://scotthelme.co.uk/securing-dns-across-all-of-my-devices-with-pihole-dns-over-https-1-1-1-1/)
- [Adafruit: installing Pi-hole on a Pi Zero W](https://learn.adafruit.com/pi-hole-ad-blocker-with-pi-zero-w/install-pi-hole)
- [Lifehacker: Turn A Raspberry Pi Into An Ad Blocker With A Single Command](https://www.lifehacker.com.au/2015/02/turn-a-raspberry-pi-into-an-ad-blocker-with-a-single-command/) - [Lifehacker: Turn A Raspberry Pi Into An Ad Blocker With A Single Command](https://www.lifehacker.com.au/2015/02/turn-a-raspberry-pi-into-an-ad-blocker-with-a-single-command/)
- [MakeUseOf: Adblock Everywhere: The Raspberry Pi-Hole Way](http://www.makeuseof.com/tag/adblock-everywhere-raspberry-pi-hole-way/) - [MakeUseOf: Adblock Everywhere: The Raspberry Pi-Hole Way](http://www.makeuseof.com/tag/adblock-everywhere-raspberry-pi-hole-way/)
- [Catchpoint: Ad-Blocking on Apple iOS9: Valuing the End User Experience](http://blog.catchpoint.com/2015/09/14/ad-blocking-apple/) - [Catchpoint: Ad-Blocking on Apple iOS9: Valuing the End User Experience](http://blog.catchpoint.com/2015/09/14/ad-blocking-apple/)
@ -215,3 +199,12 @@ While quite outdated at this point, [this original blog post about Pi-hole](http
- [CryptoAUSTRALIA: How We Tried 5 Privacy Focused Raspberry Pi Projects](https://blog.cryptoaustralia.org.au/2017/10/05/5-privacy-focused-raspberry-pi-projects/) - [CryptoAUSTRALIA: How We Tried 5 Privacy Focused Raspberry Pi Projects](https://blog.cryptoaustralia.org.au/2017/10/05/5-privacy-focused-raspberry-pi-projects/)
- [CryptoAUSTRALIA: Pi-hole Workshop](https://blog.cryptoaustralia.org.au/2017/11/02/pi-hole-network-wide-ad-blocker/) - [CryptoAUSTRALIA: Pi-hole Workshop](https://blog.cryptoaustralia.org.au/2017/11/02/pi-hole-network-wide-ad-blocker/)
- [Know How 355: Killing ads with a Raspberry Pi-Hole!](https://www.twit.tv/shows/know-how/episodes/355) - [Know How 355: Killing ads with a Raspberry Pi-Hole!](https://www.twit.tv/shows/know-how/episodes/355)
-----
## Pi-hole Projects
- [The Big Blocklist Collection](https://wally3k.github.io)
- [Pie in the Sky-Hole](https://dlaa.me/blog/post/skyhole)
- [Copernicus: Windows Tray Application](https://github.com/goldbattle/copernicus)
- [Magic Mirror with DNS Filtering](https://zonksec.com/blog/magic-mirror-dns-filtering/#dnssoftware)
- [Windows DNS Swapper](https://github.com/roots84/DNS-Swapper)

@ -1,23 +0,0 @@
# The below list amalgamates several lists we used previously.
# See `https://github.com/StevenBlack/hosts` for details
##StevenBlack's list
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
##MalwareDomains
https://mirror1.malwaredomains.com/files/justdomains
##Cameleon
http://sysctl.org/cameleon/hosts
##Zeustracker
https://zeustracker.abuse.ch/blocklist.php?download=domainblocklist
##Disconnect.me Tracking
https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
##Disconnect.me Ads
https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
##Hosts-file.net
https://hosts-file.net/ad_servers.txt

File diff suppressed because it is too large Load Diff

@ -13,10 +13,11 @@ basename=pihole
piholeDir=/etc/"${basename}" piholeDir=/etc/"${basename}"
whitelist="${piholeDir}"/whitelist.txt whitelist="${piholeDir}"/whitelist.txt
blacklist="${piholeDir}"/blacklist.txt blacklist="${piholeDir}"/blacklist.txt
readonly wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf" readonly regexlist="/etc/pihole/regex.list"
reload=false reload=false
addmode=true addmode=true
verbose=true verbose=true
wildcard=false
domList=() domList=()
@ -28,16 +29,19 @@ source ${colfile}
helpFunc() { helpFunc() {
if [[ "${listMain}" == "${whitelist}" ]]; then if [[ "${listMain}" == "${whitelist}" ]]; then
param="w" param="w"
type="white" type="white"
elif [[ "${listMain}" == "${wildcardlist}" ]]; then elif [[ "${listMain}" == "${regexlist}" && "${wildcard}" == true ]]; then
param="wild" param="-wild"
type="wildcard black" type="wildcard black"
else elif [[ "${listMain}" == "${regexlist}" ]]; then
param="b" param="-regex"
type="black" type="regex black"
fi else
param="b"
type="black"
fi
echo "Usage: pihole -${param} [options] <domain> <domain2 ...> echo "Usage: pihole -${param} [options] <domain> <domain2 ...>
Example: 'pihole -${param} site.com', or 'pihole -${param} site1.com site2.com' Example: 'pihole -${param} site.com', or 'pihole -${param} site1.com site2.com'
@ -55,212 +59,216 @@ Options:
} }
EscapeRegexp() { EscapeRegexp() {
# This way we may safely insert an arbitrary # This way we may safely insert an arbitrary
# string in our regular expressions # string in our regular expressions
# Also remove leading "." if present # This sed is intentionally executed in three steps to ease maintainability
echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" # The first sed removes any amount of leading dots
echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g"
} }
HandleOther() { HandleOther() {
# Convert to lowercase # Convert to lowercase
domain="${1,,}" domain="${1,,}"
# Check validity of domain # Check validity of domain (don't check for regex entries)
if [[ "${#domain}" -le 253 ]]; then if [[ "${#domain}" -le 253 ]]; then
validDomain=$(grep -P "^((-|_)*[a-z\d]((-|_)*[a-z\d])*(-|_)*)(\.(-|_)*([a-z\d]((-|_)*[a-z\d])*))*$" <<< "${domain}") # Valid chars check if [[ "${listMain}" == "${regexlist}" && "${wildcard}" == false ]]; then
validDomain=$(grep -P "^[^\.]{1,63}(\.[^\.]{1,63})*$" <<< "${validDomain}") # Length of each label validDomain="${domain}"
fi else
validDomain=$(grep -P "^((-|_)*[a-z\\d]((-|_)*[a-z\\d])*(-|_)*)(\\.(-|_)*([a-z\\d]((-|_)*[a-z\\d])*))*$" <<< "${domain}") # Valid chars check
if [[ -n "${validDomain}" ]]; then validDomain=$(grep -P "^[^\\.]{1,63}(\\.[^\\.]{1,63})*$" <<< "${validDomain}") # Length of each label
domList=("${domList[@]}" ${validDomain}) fi
else fi
echo -e " ${CROSS} ${domain} is not a valid argument or domain name!"
fi if [[ -n "${validDomain}" ]]; then
domList=("${domList[@]}" ${validDomain})
else
echo -e " ${CROSS} ${domain} is not a valid argument or domain name!"
fi
} }
PoplistFile() { PoplistFile() {
# Check whitelist file exists, and if not, create it # Check whitelist file exists, and if not, create it
if [[ ! -f "${whitelist}" ]]; then if [[ ! -f "${whitelist}" ]]; then
touch "${whitelist}" touch "${whitelist}"
fi fi
# Check blacklist file exists, and if not, create it # Check blacklist file exists, and if not, create it
if [[ ! -f "${blacklist}" ]]; then if [[ ! -f "${blacklist}" ]]; then
touch "${blacklist}" touch "${blacklist}"
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 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}"
else
RemoveDomain "${dom}" "${listMain}"
fi
done done
} }
AddDomain() { AddDomain() {
list="$2" list="$2"
domain=$(EscapeRegexp "$1") domain=$(EscapeRegexp "$1")
[[ "${list}" == "${whitelist}" ]] && listname="whitelist" [[ "${list}" == "${whitelist}" ]] && listname="whitelist"
[[ "${list}" == "${blacklist}" ]] && listname="blacklist" [[ "${list}" == "${blacklist}" ]] && listname="blacklist"
[[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist"
if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then
if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then [[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only"
[[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only" [[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only"
[[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only" bool=true
bool=true # Is the domain in the list we want to add it to?
# Is the domain in the list we want to add it to? grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false
grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false
if [[ "${bool}" == false ]]; then
if [[ "${bool}" == false ]]; then # Domain not found in the whitelist file, add it!
# Domain not found in the whitelist file, add it! if [[ "${verbose}" == true ]]; then
if [[ "${verbose}" == true ]]; then echo -e " ${INFO} Adding ${1} to ${listname}..."
echo -e " ${INFO} Adding $1 to $listname..." fi
fi reload=true
reload=true # Add it to the list we want to add it to
# Add it to the list we want to add it to echo "$1" >> "${list}"
echo "$1" >> "${list}" else
else if [[ "${verbose}" == true ]]; then
if [[ "${verbose}" == true ]]; then echo -e " ${INFO} ${1} already exists in ${listname}, no need to add!"
echo -e " ${INFO} ${1} already exists in ${listname}, no need to add!" fi
fi fi
elif [[ "${list}" == "${regexlist}" ]]; then
[[ -z "${type}" ]] && type="--wildcard-only"
bool=true
domain="${1}"
[[ "${wildcard}" == true ]] && domain="(^|\\.)${domain//\./\\.}$"
# Is the domain in the list?
# Search only for exactly matching lines
grep -Fx "${domain}" "${regexlist}" > /dev/null 2>&1 || bool=false
if [[ "${bool}" == false ]]; then
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} Adding ${domain} to regex list..."
fi
reload="restart"
echo "$domain" >> "${regexlist}"
else
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} ${domain} already exists in regex list, no need to add!"
fi
fi
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%/*}
[[ -z "${type}" ]] && type="--wildcard-only"
bool=true
# Is the domain in the list?
grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false
if [[ "${bool}" == false ]]; then
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} Adding $1 to wildcard blacklist..."
fi
reload="restart"
echo "address=/$1/${IPV4_ADDRESS}" >> "${wildcardlist}"
if [[ "${#IPV6_ADDRESS}" > 0 ]]; then
echo "address=/$1/${IPV6_ADDRESS}" >> "${wildcardlist}"
fi
else
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} ${1} already exists in wildcard blacklist, no need to add!"
fi
fi
fi
} }
RemoveDomain() { RemoveDomain() {
list="$2" list="$2"
domain=$(EscapeRegexp "$1") domain=$(EscapeRegexp "$1")
[[ "${list}" == "${whitelist}" ]] && listname="whitelist" [[ "${list}" == "${whitelist}" ]] && listname="whitelist"
[[ "${list}" == "${blacklist}" ]] && listname="blacklist" [[ "${list}" == "${blacklist}" ]] && listname="blacklist"
[[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist"
if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then
if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then bool=true
bool=true [[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only"
[[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only" [[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only"
[[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only" # Is it in the list? Logic follows that if its whitelisted it should not be blacklisted and vice versa
# 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
grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false if [[ "${bool}" == true ]]; then
if [[ "${bool}" == true ]]; then # Remove it from the other one
# Remove it from the other one echo -e " ${INFO} Removing $1 from ${listname}..."
echo -e " ${INFO} Removing $1 from $listname..." # /I flag: search case-insensitive
# /I flag: search case-insensitive sed -i "/${domain}/Id" "${list}"
sed -i "/${domain}/Id" "${list}" reload=true
reload=true else
else if [[ "${verbose}" == true ]]; then
if [[ "${verbose}" == true ]]; then echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!"
echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!" fi
fi fi
elif [[ "${list}" == "${regexlist}" ]]; then
[[ -z "${type}" ]] && type="--wildcard-only"
domain="${1}"
[[ "${wildcard}" == true ]] && domain="(^|\\.)${domain//\./\\.}$"
bool=true
# Is it in the list?
grep -Fx "${domain}" "${regexlist}" > /dev/null 2>&1 || bool=false
if [[ "${bool}" == true ]]; then
# Remove it from the other one
echo -e " ${INFO} Removing $domain from regex list..."
local lineNumber
lineNumber=$(grep -Fnx "$domain" "${list}" | cut -f1 -d:)
sed -i "${lineNumber}d" "${list}"
reload=true
else
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} ${domain} does not exist in regex list, no need to remove!"
fi
fi
fi fi
elif [[ "${list}" == "${wildcardlist}" ]]; then
[[ -z "${type}" ]] && type="--wildcard-only"
bool=true
# Is it in the list?
grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false
if [[ "${bool}" == true ]]; then
# Remove it from the other one
echo -e " ${INFO} Removing $1 from $listname..."
# /I flag: search case-insensitive
sed -i "/address=\/${domain}/Id" "${list}"
reload=true
else
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!"
fi
fi
fi
} }
# Update Gravity # Update Gravity
Reload() { Reload() {
echo "" echo ""
pihole -g --skip-download "${type:-}" pihole -g --skip-download "${type:-}"
} }
Displaylist() { Displaylist() {
if [[ -f ${listMain} ]]; then if [[ -f ${listMain} ]]; then
if [[ "${listMain}" == "${whitelist}" ]]; then if [[ "${listMain}" == "${whitelist}" ]]; then
string="gravity resistant domains" string="gravity resistant domains"
else
string="domains caught in the sinkhole"
fi
verbose=false
echo -e "Displaying $string:\n"
count=1
while IFS= read -r RD || [ -n "${RD}" ]; do
echo " ${count}: ${RD}"
count=$((count+1))
done < "${listMain}"
else else
string="domains caught in the sinkhole" echo -e " ${COL_LIGHT_RED}${listMain} does not exist!${COL_NC}"
fi fi
verbose=false exit 0;
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;
} }
NukeList() { NukeList() {
if [[ -f "${listMain}" ]]; then if [[ -f "${listMain}" ]]; then
# Back up original list # Back up original list
cp "${listMain}" "${listMain}.bck~" cp "${listMain}" "${listMain}.bck~"
# Empty out file # Empty out file
echo "" > "${listMain}" echo "" > "${listMain}"
fi fi
} }
for var in "$@"; do for var in "$@"; do
case "${var}" in case "${var}" in
"-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";; "-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";;
"-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";; "-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";;
"-wild" | "wildcard" ) listMain="${wildcardlist}";; "--wild" | "wildcard" ) listMain="${regexlist}"; wildcard=true;;
"-nr"| "--noreload" ) reload=false;; "--regex" | "regex" ) listMain="${regexlist}";;
"-d" | "--delmode" ) addmode=false;; "-nr"| "--noreload" ) reload=false;;
"-q" | "--quiet" ) verbose=false;; "-d" | "--delmode" ) addmode=false;;
"-h" | "--help" ) helpFunc;; "-q" | "--quiet" ) verbose=false;;
"-l" | "--list" ) Displaylist;; "-h" | "--help" ) helpFunc;;
"--nuke" ) NukeList;; "-l" | "--list" ) Displaylist;;
* ) HandleOther "${var}";; "--nuke" ) NukeList;;
esac * ) HandleOther "${var}";;
esac
done done
shift shift
if [[ $# = 0 ]]; then if [[ $# = 0 ]]; then
helpFunc helpFunc
fi fi
PoplistFile PoplistFile
if [[ "${reload}" != false ]]; then if [[ "${reload}" != false ]]; then
# Ensure that "restart" is used for Wildcard updates # Ensure that "restart" is used for Wildcard updates
Reload "${reload}" Reload "${reload}"
fi fi

@ -17,346 +17,179 @@ source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh"
# piholeGitURL set in basic-install.sh # piholeGitURL set in basic-install.sh
# is_repo() sourced from basic-install.sh # is_repo() sourced from basic-install.sh
# setupVars set in basic-install.sh # setupVars set in basic-install.sh
# check_download_exists sourced from basic-install.sh
# fully_fetch_repo sourced from basic-install.sh
# get_available_branches sourced from basic-install.sh
# fetch_checkout_pull_branch sourced from basic-install.sh
# checkout_pull_branch sourced from basic-install.sh
source "${setupVars}" 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}"
cd "${directory}" || return 1
if is_repo "${directory}"; then
git remote set-branches origin '*' || return 1
git fetch --quiet || return 1
else
return 1
fi
return 0
}
get_available_branches() {
# Return available branches
local directory
directory="${1}"
local output
cd "${directory}" || return 1
# 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
directory="${1}"
local branch
branch="${2}"
# Set the reference for the requested branch, fetch, check it put and pull it
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
directory="${1}"
local branch
branch="${2}"
local oldbranch
cd "${directory}" || return 1
oldbranch="$(git symbolic-ref HEAD)"
str="Switching to branch: '${branch}' from '${oldbranch}'"
echo -ne " ${INFO} $str"
git checkout "${branch}" --quiet || return 1
echo -e "${OVER} ${TICK} $str"
if [[ "$(git diff "${oldbranch}" | grep -c "^")" -gt "0" ]]; then
update="true"
fi
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() { warning1() {
echo " Please note that changing branches severely alters your Pi-hole subsystems" 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 " Features that work on the master branch, may not on a development branch"
echo -e " ${COL_LIGHT_RED}This feature is NOT supported unless a Pi-hole developer explicitly asks!${COL_NC}" echo -e " ${COL_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 read -r -p " Have you read and understood this? [y/N] " response
case "${response}" in case "${response}" in
[yY][eE][sS]|[yY]) [yY][eE][sS]|[yY])
echo "" echo ""
return 0 return 0
;; ;;
*) *)
echo -e "\\n ${INFO} Branch change has been cancelled" echo -e "\\n ${INFO} Branch change has been cancelled"
return 1 return 1
;; ;;
esac esac
} }
checkout() { checkout() {
local corebranches local corebranches
local webbranches local webbranches
# Avoid globbing # Avoid globbing
set -f set -f
# This is unlikely # This is unlikely
if ! is_repo "${PI_HOLE_FILES_DIR}" ; then if ! is_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e " ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system! echo -e " ${COL_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}" echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}"
exit 1; exit 1;
fi
if [[ "${INSTALL_WEB}" == "true" ]]; then
if ! is_repo "${webInterfaceDir}" ; then
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
fi if [[ "${INSTALL_WEB_INTERFACE}" == "true" ]]; then
if ! is_repo "${webInterfaceDir}" ; then
if [[ -z "${1}" ]]; then echo -e " ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!"
echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC} echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}"
Try 'pihole checkout --help' for more information." exit 1;
exit 1 fi
fi
if ! warning1 ; then
exit 1
fi
if [[ "${1}" == "dev" ]] ; then
# Shortcut to check out development branches
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 fi
#echo -e " ${TICK} Pi-hole Core"
get_binary_name if [[ -z "${1}" ]]; then
local path echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC}"
path="development/${binary}" echo -e " Try 'pihole checkout --help' for more information."
echo "development" > /etc/pihole/ftlbranch exit 1
FTLinstall "${binary}" "${path}"
elif [[ "${1}" == "master" ]] ; then
# Shortcut to check out master branches
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 -e " ${INFO} Web interface"
fetch_checkout_pull_branch "${webInterfaceDir}" "master" || { echo " ${CROSS} Unable to pull Web master branch"; exit 1; }
fi
#echo -e " ${TICK} Web Interface"
get_binary_name
local path
path="master/${binary}"
echo "master" > /etc/pihole/ftlbranch
FTLinstall "${binary}" "${path}"
elif [[ "${1}" == "core" ]] ; then
str="Fetching branches from ${piholeGitUrl}"
echo -ne " ${INFO} $str"
if ! fully_fetch_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e "${OVER} ${CROSS} $str"
exit 1
fi
corebranches=($(get_available_branches "${PI_HOLE_FILES_DIR}"))
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 -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
str="Fetching branches from ${webInterfaceGitUrl}"
echo -ne " ${INFO} $str"
if ! fully_fetch_repo "${webInterfaceDir}" ; then
echo -e "${OVER} ${CROSS} $str"
exit 1
fi fi
webbranches=($(get_available_branches "${webInterfaceDir}"))
if [[ "${webbranches[*]}" == *"master"* ]]; then if ! warning1 ; then
echo -e "${OVER} ${TICK} $str exit 1
${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 fi
echo "" if [[ "${1}" == "dev" ]] ; then
# Have the user choose the branch they want # Shortcut to check out development branches
if ! (for e in "${webbranches[@]}"; do [[ "$e" == "${2}" ]] && exit 0; done); then echo -e " ${INFO} Shortcut \"dev\" detected - checking out development / devel branches..."
echo -e " ${INFO} Requested branch \"${2}\" is not available" echo ""
echo -e " ${INFO} Available branches for Web Admin are:" echo -e " ${INFO} Pi-hole Core"
for e in "${webbranches[@]}"; do echo " - $e"; done fetch_checkout_pull_branch "${PI_HOLE_FILES_DIR}" "development" || { echo " ${CROSS} Unable to pull Core developement branch"; exit 1; }
exit 1 if [[ "${INSTALL_WEB_INTERFACE}" == "true" ]]; then
fi echo ""
checkout_pull_branch "${webInterfaceDir}" "${2}" echo -e " ${INFO} Web interface"
elif [[ "${1}" == "ftl" ]] ; then fetch_checkout_pull_branch "${webInterfaceDir}" "devel" || { echo " ${CROSS} Unable to pull Web development branch"; exit 1; }
get_binary_name fi
local path #echo -e " ${TICK} Pi-hole Core"
path="${2}/${binary}"
get_binary_name
local path
path="development/${binary}"
echo "development" > /etc/pihole/ftlbranch
elif [[ "${1}" == "master" ]] ; then
# Shortcut to check out master branches
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_INTERFACE} == "true" ]]; then
echo -e " ${INFO} Web interface"
fetch_checkout_pull_branch "${webInterfaceDir}" "master" || { echo " ${CROSS} Unable to pull Web master branch"; exit 1; }
fi
#echo -e " ${TICK} Web Interface"
get_binary_name
local path
path="master/${binary}"
echo "master" > /etc/pihole/ftlbranch
elif [[ "${1}" == "core" ]] ; then
str="Fetching branches from ${piholeGitUrl}"
echo -ne " ${INFO} $str"
if ! fully_fetch_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e "${OVER} ${CROSS} $str"
exit 1
fi
corebranches=($(get_available_branches "${PI_HOLE_FILES_DIR}"))
if [[ "${corebranches[*]}" == *"master"* ]]; then
echo -e "${OVER} ${TICK} $str"
echo -e "${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 -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_INTERFACE}" == "true" ]] ; then
str="Fetching branches from ${webInterfaceGitUrl}"
echo -ne " ${INFO} $str"
if ! fully_fetch_repo "${webInterfaceDir}" ; then
echo -e "${OVER} ${CROSS} $str"
exit 1
fi
webbranches=($(get_available_branches "${webInterfaceDir}"))
if [[ "${webbranches[*]}" == *"master"* ]]; then
echo -e "${OVER} ${TICK} $str"
echo -e "${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 -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"
echo "${2}" > /etc/pihole/ftlbranch
FTLinstall "${binary}"
start_service pihole-FTL
enable_service pihole-FTL
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
if check_download_exists "$path"; then
echo " ${TICK} Branch ${2} exists"
echo "${2}" > /etc/pihole/ftlbranch
FTLinstall "${binary}" "${path}"
else else
echo " ${CROSS} Requested branch \"${2}\" is not available" echo -e " ${INFO} Requested option \"${1}\" 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 exit 1
fi fi
else # Force updating everything
echo -e " ${INFO} Requested option \"${1}\" is not available" if [[ ! "${1}" == "web" && ! "${1}" == "ftl" ]]; then
exit 1 echo -e " ${INFO} Running installer to upgrade your installation"
fi if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then
exit 0
# Force updating everything else
if [[ ( ! "${1}" == "web" && ! "${1}" == "ftl" ) && "${update}" == "true" ]]; then echo -e " ${COL_LIGHT_RED} Error: Unable to complete update, please contact support${COL_NC}"
echo -e " ${INFO} Running installer to upgrade your installation" exit 1
if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then fi
exit 0
else
echo -e " ${COL_LIGHT_RED} Error: Unable to complete update, please contact support${COL_NC}"
exit 1
fi fi
fi
} }

File diff suppressed because it is too large Load Diff

@ -16,48 +16,51 @@ source ${colfile}
# Constructed to return nothing when # Constructed to return nothing when
# a) the setting is not present in the config file, or # a) the setting is not present in the config file, or
# b) the setting is commented out (e.g. "#DBFILE=...") # b) the setting is commented out (e.g. "#DBFILE=...")
DBFILE="$(sed -n -e 's/^\s^.DBFILE\s*=\s*//p' /etc/pihole/pihole-FTL.conf)" FTLconf="/etc/pihole/pihole-FTL.conf"
if [ -e "$FTLconf" ]; then
DBFILE="$(sed -n -e 's/^\s*DBFILE\s*=\s*//p' ${FTLconf})"
fi
# Test for empty string. Use standard path in this case. # Test for empty string. Use standard path in this case.
if [ -z "$DBFILE" ]; then if [ -z "$DBFILE" ]; then
DBFILE="/etc/pihole/pihole-FTL.db" DBFILE="/etc/pihole/pihole-FTL.db"
fi fi
if [[ "$@" != *"quiet"* ]]; then if [[ "$@" != *"quiet"* ]]; then
echo -ne " ${INFO} Flushing /var/log/pihole.log ..." echo -ne " ${INFO} Flushing /var/log/pihole.log ..."
fi fi
if [[ "$@" == *"once"* ]]; then if [[ "$@" == *"once"* ]]; then
# Nightly logrotation # Nightly logrotation
if command -v /usr/sbin/logrotate >/dev/null; then if command -v /usr/sbin/logrotate >/dev/null; then
# Logrotate once # Logrotate once
/usr/sbin/logrotate --force /etc/pihole/logrotate /usr/sbin/logrotate --force /etc/pihole/logrotate
else else
# Copy pihole.log over to pihole.log.1 # Copy pihole.log over to pihole.log.1
# and empty out pihole.log # and empty out pihole.log
# Note that moving the file is not an option, as # Note that moving the file is not an option, as
# dnsmasq would happily continue writing into the # dnsmasq would happily continue writing into the
# moved file (it will have the same file handler) # moved file (it will have the same file handler)
cp /var/log/pihole.log /var/log/pihole.log.1 cp /var/log/pihole.log /var/log/pihole.log.1
echo " " > /var/log/pihole.log echo " " > /var/log/pihole.log
fi fi
else else
# Manual flushing # Manual flushing
if command -v /usr/sbin/logrotate >/dev/null; then if command -v /usr/sbin/logrotate >/dev/null; then
# Logrotate twice to move all data out of sight of FTL # 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; sleep 3
/usr/sbin/logrotate --force /etc/pihole/logrotate /usr/sbin/logrotate --force /etc/pihole/logrotate
else else
# Flush both pihole.log and pihole.log.1 (if existing) # Flush both pihole.log and pihole.log.1 (if existing)
echo " " > /var/log/pihole.log echo " " > /var/log/pihole.log
if [ -f /var/log/pihole.log.1 ]; then if [ -f /var/log/pihole.log.1 ]; then
echo " " > /var/log/pihole.log.1 echo " " > /var/log/pihole.log.1
fi
fi fi
fi # Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history)
# Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history) deleted=$(sqlite3 "${DBFILE}" "DELETE FROM queries WHERE timestamp >= strftime('%s','now')-86400; select changes() from queries limit 1")
deleted=$(sqlite3 "${DBFILE}" "DELETE FROM queries WHERE timestamp >= strftime('%s','now')-86400; select changes() from queries limit 1")
fi fi
if [[ "$@" != *"quiet"* ]]; then if [[ "$@" != *"quiet"* ]]; then
echo -e "${OVER} ${TICK} Flushed /var/log/pihole.log" echo -e "${OVER} ${TICK} Flushed /var/log/pihole.log"
echo -e " ${TICK} Deleted ${deleted} queries from database" echo -e " ${TICK} Deleted ${deleted} queries from database"
fi fi

@ -0,0 +1,239 @@
#!/usr/bin/env bash
# shellcheck disable=SC1090
# Pi-hole: A black hole for Internet advertisements
# (c) 2018 Pi-hole, LLC (https://pi-hole.net)
# Network-wide ad blocking via your own hardware.
#
# Query Domain Lists
#
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
# Globals
piholeDir="/etc/pihole"
adListsList="$piholeDir/adlists.list"
wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf"
options="$*"
adlist=""
all=""
exact=""
blockpage=""
matchType="match"
colfile="/opt/pihole/COL_TABLE"
source "${colfile}"
# Print each subdomain
# e.g: foo.bar.baz.com = "foo.bar.baz.com bar.baz.com baz.com com"
processWildcards() {
IFS="." read -r -a array <<< "${1}"
for (( i=${#array[@]}-1; i>=0; i-- )); do
ar=""
for (( j=${#array[@]}-1; j>${#array[@]}-i-2; j-- )); do
if [[ $j == $((${#array[@]}-1)) ]]; then
ar="${array[$j]}"
else
ar="${array[$j]}.${ar}"
fi
done
echo "${ar}"
done
}
# Scan an array of files for matching strings
scanList(){
# Escape full stops
local domain="${1//./\\.}" lists="${2}" type="${3:-}"
# Prevent grep from printing file path
cd "$piholeDir" || exit 1
# Prevent grep -i matching slowly: http://bit.ly/2xFXtUX
export LC_CTYPE=C
# /dev/null forces filename to be printed when only one list has been generated
# shellcheck disable=SC2086
case "${type}" in
"exact" ) grep -i -E -l "(^|\\s)${domain}($|\\s|#)" ${lists} /dev/null 2>/dev/null;;
"wc" ) grep -i -o -m 1 "/${domain}/" ${lists} 2>/dev/null;;
* ) grep -i "${domain}" ${lists} /dev/null 2>/dev/null;;
esac
}
if [[ "${options}" == "-h" ]] || [[ "${options}" == "--help" ]]; then
echo "Usage: pihole -q [option] <domain>
Example: 'pihole -q -exact domain.com'
Query the adlists for a specified domain
Options:
-adlist Print the name of the block list URL
-exact Search the block lists for exact domain matches
-all Return all query matches within a block list
-h, --help Show this help dialog"
exit 0
fi
if [[ ! -e "$adListsList" ]]; then
echo -e "${COL_LIGHT_RED}The file $adListsList was not found${COL_NC}"
exit 1
fi
# Handle valid options
if [[ "${options}" == *"-bp"* ]]; then
exact="exact"; blockpage=true
else
[[ "${options}" == *"-adlist"* ]] && adlist=true
[[ "${options}" == *"-all"* ]] && all=true
if [[ "${options}" == *"-exact"* ]]; then
exact="exact"; matchType="exact ${matchType}"
fi
fi
# Strip valid options, leaving only the domain and invalid options
# This allows users to place the options before or after the domain
options=$(sed -E 's/ ?-(bp|adlists?|all|exact) ?//g' <<< "${options}")
# Handle remaining options
# If $options contain non ASCII characters, convert to punycode
case "${options}" in
"" ) str="No domain specified";;
*" "* ) str="Unknown query option specified";;
*[![:ascii:]]* ) domainQuery=$(idn2 "${options}");;
* ) domainQuery="${options}";;
esac
if [[ -n "${str:-}" ]]; then
echo -e "${str}${COL_NC}\\nTry 'pihole -q --help' for more information."
exit 1
fi
# Scan Whitelist and Blacklist
lists="whitelist.txt blacklist.txt"
mapfile -t results <<< "$(scanList "${domainQuery}" "${lists}" "${exact}")"
if [[ -n "${results[*]}" ]]; then
wbMatch=true
# Loop through each result in order to print unique file title once
for result in "${results[@]}"; do
fileName="${result%%.*}"
if [[ -n "${blockpage}" ]]; then
echo "π ${result}"
exit 0
elif [[ -n "${exact}" ]]; then
echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}"
else
# Only print filename title once per file
if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then
echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}"
fileName_prev="${fileName}"
fi
echo " ${result#*:}"
fi
done
fi
# Scan Wildcards
if [[ -e "${wildcardlist}" ]]; then
# Determine all subdomains, domain and TLDs
mapfile -t wildcards <<< "$(processWildcards "${domainQuery}")"
for match in "${wildcards[@]}"; do
# Search wildcard list for matches
mapfile -t results <<< "$(scanList "${match}" "${wildcardlist}" "wc")"
if [[ -n "${results[*]}" ]]; then
if [[ -z "${wcMatch:-}" ]] && [[ -z "${blockpage}" ]]; then
wcMatch=true
echo " ${matchType^} found in ${COL_BOLD}Wildcards${COL_NC}:"
fi
case "${blockpage}" in
true ) echo "π ${wildcardlist##*/}"; exit 0;;
* ) echo " *.${match}";;
esac
fi
done
fi
# Get version sorted *.domains filenames (without dir path)
lists=("$(cd "$piholeDir" || exit 0; printf "%s\\n" -- *.domains | sort -V)")
# Query blocklists for occurences of domain
mapfile -t results <<< "$(scanList "${domainQuery}" "${lists[*]}" "${exact}")"
# Handle notices
if [[ -z "${wbMatch:-}" ]] && [[ -z "${wcMatch:-}" ]] && [[ -z "${results[*]}" ]]; then
echo -e " ${INFO} No ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC} within the block lists"
exit 0
elif [[ -z "${results[*]}" ]]; then
# Result found in WL/BL/Wildcards
exit 0
elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 100 ]]; then
echo -e " ${INFO} Over 100 ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC}
This can be overridden using the -all option"
exit 0
fi
# Remove unwanted content from non-exact $results
if [[ -z "${exact}" ]]; then
# Delete lines starting with #
# Remove comments after domain
# Remove hosts format IP address
mapfile -t results <<< "$(IFS=$'\n'; sed \
-e "/:#/d" \
-e "s/[ \\t]#.*//g" \
-e "s/:.*[ \\t]/:/g" \
<<< "${results[*]}")"
# Exit if result was in a comment
[[ -z "${results[*]}" ]] && exit 0
fi
# Get adlist file content as array
if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then
for adlistUrl in $(< "${adListsList}"); do
if [[ "${adlistUrl:0:4}" =~ (http|www.) ]]; then
adlists+=("${adlistUrl}")
fi
done
fi
# Print "Exact matches for" title
if [[ -n "${exact}" ]] && [[ -z "${blockpage}" ]]; then
plural=""; [[ "${#results[*]}" -gt 1 ]] && plural="es"
echo " ${matchType^}${plural} for ${COL_BOLD}${domainQuery}${COL_NC} found in:"
fi
for result in "${results[@]}"; do
fileName="${result/:*/}"
# Determine *.domains URL using filename's number
if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then
fileNum="${fileName/list./}"; fileNum="${fileNum%%.*}"
fileName="${adlists[$fileNum]}"
# Discrepency occurs when adlists has been modified, but Gravity has not been run
if [[ -z "${fileName}" ]]; then
fileName="${COL_LIGHT_RED}(no associated adlists URL found)${COL_NC}"
fi
fi
if [[ -n "${blockpage}" ]]; then
echo "${fileNum} ${fileName}"
elif [[ -n "${exact}" ]]; then
echo " ${fileName}"
else
if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then
count=""
echo " ${matchType^} found in ${COL_BOLD}${fileName}${COL_NC}:"
fileName_prev="${fileName}"
fi
: $((count++))
# Print matching domain if $max_count has not been reached
[[ -z "${all}" ]] && max_count="50"
if [[ -z "${all}" ]] && [[ "${count}" -ge "${max_count}" ]]; then
[[ "${count}" -gt "${max_count}" ]] && continue
echo " ${COL_GRAY}Over ${count} results found, skipping rest of file${COL_NC}"
else
echo " ${result#*:}"
fi
fi
done
exit 0

@ -15,28 +15,28 @@
# Borrowed from adafruit-pitft-helper < borrowed from raspi-config # Borrowed from adafruit-pitft-helper < borrowed from raspi-config
# https://github.com/adafruit/Adafruit-PiTFT-Helper/blob/master/adafruit-pitft-helper#L324-L334 # https://github.com/adafruit/Adafruit-PiTFT-Helper/blob/master/adafruit-pitft-helper#L324-L334
getInitSys() { getInitSys() {
if command -v systemctl > /dev/null && systemctl | grep -q '\-\.mount'; then if command -v systemctl > /dev/null && systemctl | grep -q '\-\.mount'; then
SYSTEMD=1 SYSTEMD=1
elif [ -f /etc/init.d/cron ] && [ ! -h /etc/init.d/cron ]; then elif [ -f /etc/init.d/cron ] && [ ! -h /etc/init.d/cron ]; then
SYSTEMD=0 SYSTEMD=0
else else
echo "Unrecognised init system" echo "Unrecognised init system"
return 1 return 1
fi fi
} }
# Borrowed from adafruit-pitft-helper: # Borrowed from adafruit-pitft-helper:
# https://github.com/adafruit/Adafruit-PiTFT-Helper/blob/master/adafruit-pitft-helper#L274-L285 # https://github.com/adafruit/Adafruit-PiTFT-Helper/blob/master/adafruit-pitft-helper#L274-L285
autoLoginPiToConsole() { autoLoginPiToConsole() {
if [ -e /etc/init.d/lightdm ]; then if [ -e /etc/init.d/lightdm ]; then
if [ ${SYSTEMD} -eq 1 ]; then if [ ${SYSTEMD} -eq 1 ]; then
systemctl set-default multi-user.target systemctl set-default multi-user.target
ln -fs /etc/systemd/system/autologin@.service /etc/systemd/system/getty.target.wants/getty@tty1.service ln -fs /etc/systemd/system/autologin@.service /etc/systemd/system/getty.target.wants/getty@tty1.service
else else
update-rc.d lightdm disable 2 update-rc.d lightdm disable 2
sed /etc/inittab -i -e "s/1:2345:respawn:\/sbin\/getty --noclear 38400 tty1/1:2345:respawn:\/bin\/login -f pi tty1 <\/dev\/tty1 >\/dev\/tty1 2>&1/" sed /etc/inittab -i -e "s/1:2345:respawn:\/sbin\/getty --noclear 38400 tty1/1:2345:respawn:\/bin\/login -f pi tty1 <\/dev\/tty1 >\/dev\/tty1 2>&1/"
fi fi
fi fi
} }
######### SCRIPT ########### ######### SCRIPT ###########

@ -19,6 +19,9 @@ readonly PI_HOLE_FILES_DIR="/etc/.pihole"
# shellcheck disable=SC2034 # shellcheck disable=SC2034
PH_TEST=true PH_TEST=true
# when --check-only is passed to this script, it will not perform the actual update
CHECK_ONLY=false
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
@ -28,201 +31,161 @@ source "/opt/pihole/COL_TABLE"
# make_repo() sourced from basic-install.sh # make_repo() sourced from basic-install.sh
# update_repo() source from basic-install.sh # update_repo() source from basic-install.sh
# getGitFiles() sourced from basic-install.sh # getGitFiles() sourced from basic-install.sh
# get_binary_name() sourced from basic-install.sh
# FTLcheckUpdate() sourced from basic-install.sh
GitCheckUpdateAvail() { GitCheckUpdateAvail() {
local directory="${1}" local directory
curdir=$PWD directory="${1}"
cd "${directory}" || return curdir=$PWD
cd "${directory}" || return
# Fetch latest changes in this repo
git fetch --quiet origin # Fetch latest changes in this repo
git fetch --quiet origin
# @ alone is a shortcut for HEAD. Older versions of git
# need @{0} # @ alone is a shortcut for HEAD. Older versions of git
LOCAL="$(git rev-parse "@{0}")" # need @{0}
LOCAL="$(git rev-parse "@{0}")"
# The suffix @{upstream} to a branchname
# (short form <branchname>@{u}) refers # The suffix @{upstream} to a branchname
# to the branch that the branch specified # (short form <branchname>@{u}) refers
# by branchname is set to build on top of# # to the branch that the branch specified
# (configured with branch.<name>.remote and # by branchname is set to build on top of#
# branch.<name>.merge). A missing branchname # (configured with branch.<name>.remote and
# defaults to the current one. # branch.<name>.merge). A missing branchname
REMOTE="$(git rev-parse "@{upstream}")" # defaults to the current one.
REMOTE="$(git rev-parse "@{upstream}")"
if [[ "${#LOCAL}" == 0 ]]; then
echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support if [[ "${#LOCAL}" == 0 ]]; then
Additional debugging output:${COL_NC}" echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support"
git status echo -e " Additional debugging output:${COL_NC}"
exit git status
fi exit
if [[ "${#REMOTE}" == 0 ]]; then fi
echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support if [[ "${#REMOTE}" == 0 ]]; then
Additional debugging output:${COL_NC}" echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support"
git status echo -e " Additional debugging output:${COL_NC}"
exit git status
fi exit
fi
# Change back to original directory
cd "${curdir}" || exit # Change back to original directory
cd "${curdir}" || exit
if [[ "${LOCAL}" != "${REMOTE}" ]]; then
# Local branch is behind remote branch -> Update
return 0
else
# Local branch is up-to-date or in a situation
# where this updater cannot be used (like on a
# branch that exists only locally)
return 1
fi
}
FTLcheckUpdate() { if [[ "${LOCAL}" != "${REMOTE}" ]]; then
local FTLversion # Local branch is behind remote branch -> Update
FTLversion=$(/usr/bin/pihole-FTL tag) return 0
local FTLlatesttag else
FTLlatesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep 'Location' | awk -F '/' '{print $NF}' | tr -d '\r\n') # Local branch is up-to-date or in a situation
# where this updater cannot be used (like on a
if [[ "${FTLversion}" != "${FTLlatesttag}" ]]; then # branch that exists only locally)
return 0 return 1
else fi
return 1
fi
} }
main() { main() {
local pihole_version_current local basicError="\\n ${COL_LIGHT_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}"
local web_version_current local core_update
local basicError="\\n ${COL_LIGHT_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}" local web_update
local FTL_update
# shellcheck disable=1090,2154
source "${setupVars}"
# This is unlikely
if ! is_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e "\\n ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!
Please re-run install script from https://pi-hole.net${COL_NC}"
exit 1;
fi
echo -e " ${INFO} Checking for updates..."
if GitCheckUpdateAvail "${PI_HOLE_FILES_DIR}" ; then
core_update=true
echo -e " ${INFO} Pi-hole Core:\\t${COL_YELLOW}update available${COL_NC}"
else
core_update=false
echo -e " ${INFO} Pi-hole Core:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
if FTLcheckUpdate ; then core_update=false
FTL_update=true web_update=false
echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}"
else
FTL_update=false FTL_update=false
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
# Logic: Don't update FTL when there is a core update available # shellcheck disable=1090,2154
# since the core update will run the installer which will itself source "${setupVars}"
# re-install (i.e. update) FTL
if ${FTL_update} && ! ${core_update}; then
echo ""
echo -e " ${INFO} FTL out of date"
FTLdetect
echo ""
fi
if [[ "${INSTALL_WEB}" == true ]]; then # This is unlikely
if ! is_repo "${ADMIN_INTERFACE_DIR}" ; then if ! is_repo "${PI_HOLE_FILES_DIR}" ; then
echo -e "\\n ${COL_LIGHT_RED}Error: Web Admin repo is missing from system! 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}" echo -e " Please re-run install script from https://pi-hole.net${COL_NC}"
exit 1; exit 1;
fi fi
if GitCheckUpdateAvail "${ADMIN_INTERFACE_DIR}" ; then echo -e " ${INFO} Checking for updates..."
web_update=true
echo -e " ${INFO} Web Interface:\\t${COL_YELLOW}update available${COL_NC}" if GitCheckUpdateAvail "${PI_HOLE_FILES_DIR}" ; then
core_update=true
echo -e " ${INFO} Pi-hole Core:\\t${COL_YELLOW}update available${COL_NC}"
else else
web_update=false core_update=false
echo -e " ${INFO} Web Interface:\\t${COL_LIGHT_GREEN}up to date${COL_NC}" echo -e " ${INFO} Pi-hole Core:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then
if ! is_repo "${ADMIN_INTERFACE_DIR}" ; then
echo -e "\\n ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!"
echo -e " Please re-run install script from https://pi-hole.net${COL_NC}"
exit 1;
fi
if GitCheckUpdateAvail "${ADMIN_INTERFACE_DIR}" ; then
web_update=true
echo -e " ${INFO} Web Interface:\\t${COL_YELLOW}update available${COL_NC}"
else
web_update=false
echo -e " ${INFO} Web Interface:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
fi fi
# Logic if FTLcheckUpdate > /dev/null; then
# If Core up to date AND web up to date: FTL_update=true
# Do nothing echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}"
# If Core up to date AND web NOT up to date: else
# Pull web repo case $? in
# If Core NOT up to date AND web up to date: 1)
# pull pihole repo, run install --unattended -- reconfigure echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
# if Core NOT up to date AND web NOT up to date: ;;
# pull pihole repo run install --unattended 2)
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Branch is not available.${COL_NC}\\n\\t\\t\\tUse ${COL_LIGHT_GREEN}pihole checkout ftl [branchname]${COL_NC} to switch to a valid branch."
if ! ${core_update} && ! ${web_update} ; then ;;
if ! ${FTL_update} ; then *)
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_RED}Something has gone wrong, contact support${COL_NC}"
esac
FTL_update=false
fi
if [[ "${core_update}" == false && "${web_update}" == false && "${FTL_update}" == false ]]; then
echo "" echo ""
echo -e " ${TICK} Everything is up to date!" echo -e " ${TICK} Everything is up to date!"
exit 0 exit 0
fi
elif ! ${core_update} && ${web_update} ; then
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 -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 -e "${basicError}" && exit 1
elif ${core_update} && ${web_update} ; then
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 -e "${basicError}" && exit 1
else
echo -e " ${COL_LIGHT_RED}Update script has malfunctioned, please contact Pi-hole Support${COL_NC}"
exit 1
fi fi
else # Web Admin not installed, so only verify if core is up to date
if ! ${core_update}; then if [[ "${CHECK_ONLY}" == true ]]; then
if ! ${FTL_update} ; then
echo "" echo ""
echo -e " ${INFO} Everything is up to date!"
exit 0 exit 0
fi
else
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 -e "${basicError}" && exit 1
fi fi
fi
if [[ "${web_update}" == true ]]; then if [[ "${core_update}" == true ]]; then
web_version_current="$(/usr/local/bin/pihole version --admin --current)" echo ""
echo "" echo -e " ${INFO} Pi-hole core files out of date, updating local repo."
echo -e " ${INFO} Web Admin version is now at ${web_version_current/* v/v} getGitFiles "${PI_HOLE_FILES_DIR}" "${PI_HOLE_GIT_URL}"
${INFO} If you had made any changes in '/var/www/html/admin/', they have been stashed using 'git stash'" echo -e " ${INFO} If you had made any changes in '/etc/.pihole/', they have been stashed using 'git stash'"
fi fi
if [[ "${core_update}" == true ]]; then if [[ "${web_update}" == true ]]; then
pihole_version_current="$(/usr/local/bin/pihole version --pihole --current)" echo ""
echo -e " ${INFO} Pi-hole Web Admin files out of date, updating local repo."
getGitFiles "${ADMIN_INTERFACE_DIR}" "${ADMIN_INTERFACE_GIT_URL}"
echo -e " ${INFO} If you had made any changes in '/var/www/html/admin/', they have been stashed using 'git stash'"
fi
if [[ "${FTL_update}" == true ]]; then
echo ""
echo -e " ${INFO} FTL out of date, it will be updated by the installer."
fi
if [[ "${FTL_update}" == true || "${core_update}" == true ]]; then
${PI_HOLE_FILES_DIR}/automated\ install/basic-install.sh --reconfigure --unattended || \
echo -e "${basicError}" && exit 1
fi
echo "" echo ""
echo -e " ${INFO} Pi-hole version is now at ${pihole_version_current/* v/v} exit 0
${INFO} If you had made any changes in '/etc/.pihole/', they have been stashed using 'git stash'"
fi
if [[ "${FTL_update}" == true ]]; then
FTL_version_current="$(/usr/bin/pihole-FTL tag)"
echo -e "\\n ${INFO} FTL version is now at ${FTL_version_current/* v/v}"
start_service pihole-FTL
enable_service pihole-FTL
fi
echo ""
exit 0
} }
if [[ "$1" == "--check-only" ]]; then
CHECK_ONLY=true
fi
main main

@ -10,57 +10,57 @@
# Credit: https://stackoverflow.com/a/46324904 # Credit: https://stackoverflow.com/a/46324904
function json_extract() { function json_extract() {
local key=$1 local key=$1
local json=$2 local json=$2
local string_regex='"([^"\]|\\.)*"' local string_regex='"([^"\]|\\.)*"'
local number_regex='-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?' local number_regex='-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?'
local value_regex="${string_regex}|${number_regex}|true|false|null" local value_regex="${string_regex}|${number_regex}|true|false|null"
local pair_regex="\"${key}\"[[:space:]]*:[[:space:]]*(${value_regex})" local pair_regex="\"${key}\"[[:space:]]*:[[:space:]]*(${value_regex})"
if [[ ${json} =~ ${pair_regex} ]]; then if [[ ${json} =~ ${pair_regex} ]]; then
echo $(sed 's/^"\|"$//g' <<< "${BASH_REMATCH[1]}") echo $(sed 's/^"\|"$//g' <<< "${BASH_REMATCH[1]}")
else else
return 1 return 1
fi fi
} }
function get_local_branch() { function get_local_branch() {
# Return active branch # Return active branch
cd "${1}" 2> /dev/null || return 1 cd "${1}" 2> /dev/null || return 1
git rev-parse --abbrev-ref HEAD || return 1 git rev-parse --abbrev-ref HEAD || return 1
} }
function get_local_version() { function get_local_version() {
# Return active branch # Return active branch
cd "${1}" 2> /dev/null || return 1 cd "${1}" 2> /dev/null || return 1
git describe --long --dirty --tags || return 1 git describe --long --dirty --tags || return 1
} }
if [[ "$2" == "remote" ]]; then if [[ "$2" == "remote" ]]; then
if [[ "$3" == "reboot" ]]; then if [[ "$3" == "reboot" ]]; then
sleep 30 sleep 30
fi fi
GITHUB_CORE_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/pi-hole/releases/latest' 2> /dev/null)")" GITHUB_CORE_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/pi-hole/releases/latest' 2> /dev/null)")"
GITHUB_WEB_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/AdminLTE/releases/latest' 2> /dev/null)")" GITHUB_WEB_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/AdminLTE/releases/latest' 2> /dev/null)")"
GITHUB_FTL_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/FTL/releases/latest' 2> /dev/null)")" GITHUB_FTL_VERSION="$(json_extract tag_name "$(curl -q 'https://api.github.com/repos/pi-hole/FTL/releases/latest' 2> /dev/null)")"
echo -n "${GITHUB_CORE_VERSION} ${GITHUB_WEB_VERSION} ${GITHUB_FTL_VERSION}" > "/etc/pihole/GitHubVersions" echo -n "${GITHUB_CORE_VERSION} ${GITHUB_WEB_VERSION} ${GITHUB_FTL_VERSION}" > "/etc/pihole/GitHubVersions"
else else
CORE_BRANCH="$(get_local_branch /etc/.pihole)" CORE_BRANCH="$(get_local_branch /etc/.pihole)"
WEB_BRANCH="$(get_local_branch /var/www/html/admin)" WEB_BRANCH="$(get_local_branch /var/www/html/admin)"
FTL_BRANCH="$(pihole-FTL branch)" FTL_BRANCH="$(pihole-FTL branch)"
echo -n "${CORE_BRANCH} ${WEB_BRANCH} ${FTL_BRANCH}" > "/etc/pihole/localbranches" echo -n "${CORE_BRANCH} ${WEB_BRANCH} ${FTL_BRANCH}" > "/etc/pihole/localbranches"
CORE_VERSION="$(get_local_version /etc/.pihole)" CORE_VERSION="$(get_local_version /etc/.pihole)"
WEB_VERSION="$(get_local_version /var/www/html/admin)" WEB_VERSION="$(get_local_version /var/www/html/admin)"
FTL_VERSION="$(pihole-FTL version)" FTL_VERSION="$(pihole-FTL version)"
echo -n "${CORE_VERSION} ${WEB_VERSION} ${FTL_VERSION}" > "/etc/pihole/localversions" echo -n "${CORE_VERSION} ${WEB_VERSION} ${FTL_VERSION}" > "/etc/pihole/localversions"
fi fi

@ -14,135 +14,135 @@ COREGITDIR="/etc/.pihole/"
WEBGITDIR="/var/www/html/admin/" WEBGITDIR="/var/www/html/admin/"
getLocalVersion() { getLocalVersion() {
# FTL requires a different method # FTL requires a different method
if [[ "$1" == "FTL" ]]; then if [[ "$1" == "FTL" ]]; then
pihole-FTL version pihole-FTL version
return 0
fi
# Get the tagged version of the local repository
local directory="${1}"
local version
cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; }
version=$(git describe --tags --always || echo "$DEFAULT")
if [[ "${version}" =~ ^v ]]; then
echo "${version}"
elif [[ "${version}" == "${DEFAULT}" ]]; then
echo "ERROR"
return 1
else
echo "Untagged"
fi
return 0 return 0
fi
# Get the tagged version of the local repository
local directory="${1}"
local version
cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; }
version=$(git describe --tags --always || echo "$DEFAULT")
if [[ "${version}" =~ ^v ]]; then
echo "${version}"
elif [[ "${version}" == "${DEFAULT}" ]]; then
echo "ERROR"
return 1
else
echo "Untagged"
fi
return 0
} }
getLocalHash() { getLocalHash() {
# Local FTL hash does not exist on filesystem # Local FTL hash does not exist on filesystem
if [[ "$1" == "FTL" ]]; then if [[ "$1" == "FTL" ]]; then
echo "N/A" echo "N/A"
return 0
fi
# Get the short hash of the local repository
local directory="${1}"
local hash
cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; }
hash=$(git rev-parse --short HEAD || echo "$DEFAULT")
if [[ "${hash}" == "${DEFAULT}" ]]; then
echo "ERROR"
return 1
else
echo "${hash}"
fi
return 0 return 0
fi
# Get the short hash of the local repository
local directory="${1}"
local hash
cd "${directory}" 2> /dev/null || { echo "${DEFAULT}"; return 1; }
hash=$(git rev-parse --short HEAD || echo "$DEFAULT")
if [[ "${hash}" == "${DEFAULT}" ]]; then
echo "ERROR"
return 1
else
echo "${hash}"
fi
return 0
} }
getRemoteHash(){ getRemoteHash(){
# Remote FTL hash is not applicable # Remote FTL hash is not applicable
if [[ "$1" == "FTL" ]]; then if [[ "$1" == "FTL" ]]; then
echo "N/A" 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 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(){ getRemoteVersion(){
# Get the version from the remote origin # Get the version from the remote origin
local daemon="${1}" local daemon="${1}"
local version 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 }' | \ awk -F: '$1 ~/tag_name/ { print $2 }' | \
tr -cd '[[:alnum:]]._-') tr -cd '[[:alnum:]]._-')
if [[ "${version}" =~ ^v ]]; then if [[ "${version}" =~ ^v ]]; then
echo "${version}" echo "${version}"
else else
echo "ERROR" echo "ERROR"
return 1 return 1
fi fi
return 0 return 0
} }
versionOutput() { versionOutput() {
[[ "$1" == "pi-hole" ]] && GITDIR=$COREGITDIR [[ "$1" == "pi-hole" ]] && GITDIR=$COREGITDIR
[[ "$1" == "AdminLTE" ]] && GITDIR=$WEBGITDIR [[ "$1" == "AdminLTE" ]] && GITDIR=$WEBGITDIR
[[ "$1" == "FTL" ]] && GITDIR="FTL" [[ "$1" == "FTL" ]] && GITDIR="FTL"
[[ "$2" == "-c" ]] || [[ "$2" == "--current" ]] || [[ -z "$2" ]] && current=$(getLocalVersion $GITDIR) [[ "$2" == "-c" ]] || [[ "$2" == "--current" ]] || [[ -z "$2" ]] && current=$(getLocalVersion $GITDIR)
[[ "$2" == "-l" ]] || [[ "$2" == "--latest" ]] || [[ -z "$2" ]] && latest=$(getRemoteVersion "$1") [[ "$2" == "-l" ]] || [[ "$2" == "--latest" ]] || [[ -z "$2" ]] && latest=$(getRemoteVersion "$1")
if [[ "$2" == "-h" ]] || [[ "$2" == "--hash" ]]; then if [[ "$2" == "-h" ]] || [[ "$2" == "--hash" ]]; then
[[ "$3" == "-c" ]] || [[ "$3" == "--current" ]] || [[ -z "$3" ]] && curHash=$(getLocalHash "$GITDIR") [[ "$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)") [[ "$3" == "-l" ]] || [[ "$3" == "--latest" ]] || [[ -z "$3" ]] && latHash=$(getRemoteHash "$1" "$(cd "$GITDIR" 2> /dev/null && git rev-parse --abbrev-ref HEAD)")
fi fi
if [[ -n "$current" ]] && [[ -n "$latest" ]]; then if [[ -n "$current" ]] && [[ -n "$latest" ]]; then
output="${1^} version is $current (Latest: $latest)" output="${1^} version is $current (Latest: $latest)"
elif [[ -n "$current" ]] && [[ -z "$latest" ]]; then elif [[ -n "$current" ]] && [[ -z "$latest" ]]; then
output="Current ${1^} version is $current" output="Current ${1^} version is $current"
elif [[ -z "$current" ]] && [[ -n "$latest" ]]; then elif [[ -z "$current" ]] && [[ -n "$latest" ]]; then
output="Latest ${1^} version is $latest" output="Latest ${1^} version is $latest"
elif [[ "$curHash" == "N/A" ]] || [[ "$latHash" == "N/A" ]]; then elif [[ "$curHash" == "N/A" ]] || [[ "$latHash" == "N/A" ]]; then
output="${1^} hash is not applicable" output="${1^} hash is not applicable"
elif [[ -n "$curHash" ]] && [[ -n "$latHash" ]]; then elif [[ -n "$curHash" ]] && [[ -n "$latHash" ]]; then
output="${1^} hash is $curHash (Latest: $latHash)" output="${1^} hash is $curHash (Latest: $latHash)"
elif [[ -n "$curHash" ]] && [[ -z "$latHash" ]]; then elif [[ -n "$curHash" ]] && [[ -z "$latHash" ]]; then
output="Current ${1^} hash is $curHash" output="Current ${1^} hash is $curHash"
elif [[ -z "$curHash" ]] && [[ -n "$latHash" ]]; then elif [[ -z "$curHash" ]] && [[ -n "$latHash" ]]; then
output="Latest ${1^} hash is $latHash" output="Latest ${1^} hash is $latHash"
else else
errorOutput errorOutput
fi fi
[[ -n "$output" ]] && echo " $output" [[ -n "$output" ]] && echo " $output"
} }
errorOutput() { errorOutput() {
echo " Invalid Option! Try 'pihole -v --help' for more information." echo " Invalid Option! Try 'pihole -v --help' for more information."
exit 1 exit 1
} }
defaultOutput() { defaultOutput() {
versionOutput "pi-hole" "$@" versionOutput "pi-hole" "$@"
versionOutput "AdminLTE" "$@" versionOutput "AdminLTE" "$@"
versionOutput "FTL" "$@" versionOutput "FTL" "$@"
} }
helpFunc() { helpFunc() {
echo "Usage: pihole -v [repo | option] [option] echo "Usage: pihole -v [repo | option] [option]
Example: 'pihole -v -p -l' Example: 'pihole -v -p -l'
Show Pi-hole, Admin Console & FTL versions Show Pi-hole, Admin Console & FTL versions
@ -150,7 +150,7 @@ Repositories:
-p, --pihole Only retrieve info regarding Pi-hole repository -p, --pihole Only retrieve info regarding Pi-hole repository
-a, --admin Only retrieve info regarding AdminLTE repository -a, --admin Only retrieve info regarding AdminLTE repository
-f, --ftl Only retrieve info regarding FTL repository -f, --ftl Only retrieve info regarding FTL repository
Options: Options:
-c, --current Return the current version -c, --current Return the current version
-l, --latest Return the latest version -l, --latest Return the latest version
@ -160,9 +160,9 @@ Options:
} }
case "${1}" in case "${1}" in
"-p" | "--pihole" ) shift; versionOutput "pi-hole" "$@";; "-p" | "--pihole" ) shift; versionOutput "pi-hole" "$@";;
"-a" | "--admin" ) shift; versionOutput "AdminLTE" "$@";; "-a" | "--admin" ) shift; versionOutput "AdminLTE" "$@";;
"-f" | "--ftl" ) shift; versionOutput "FTL" "$@";; "-f" | "--ftl" ) shift; versionOutput "FTL" "$@";;
"-h" | "--help" ) helpFunc;; "-h" | "--help" ) helpFunc;;
* ) defaultOutput "$@";; * ) defaultOutput "$@";;
esac esac

@ -13,16 +13,17 @@
readonly setupVars="/etc/pihole/setupVars.conf" readonly setupVars="/etc/pihole/setupVars.conf"
readonly dnsmasqconfig="/etc/dnsmasq.d/01-pihole.conf" readonly dnsmasqconfig="/etc/dnsmasq.d/01-pihole.conf"
readonly dhcpconfig="/etc/dnsmasq.d/02-pihole-dhcp.conf" readonly dhcpconfig="/etc/dnsmasq.d/02-pihole-dhcp.conf"
readonly FTLconf="/etc/pihole/pihole-FTL.conf"
# 03 -> wildcards # 03 -> wildcards
readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf" readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf"
coltable="/opt/pihole/COL_TABLE" coltable="/opt/pihole/COL_TABLE"
if [[ -f ${coltable} ]]; then if [[ -f ${coltable} ]]; then
source ${coltable} source ${coltable}
fi fi
helpFunc() { helpFunc() {
echo "Usage: pihole -a [options] echo "Usage: pihole -a [options]
Example: pihole -a -p password Example: pihole -a -p password
Set options for the Admin Console Set options for the Admin Console
@ -35,256 +36,278 @@ Options:
-e, email Set an administrative contact address for the Block Page -e, email Set an administrative contact address for the Block Page
-h, --help Show this help dialog -h, --help Show this help dialog
-i, interface Specify dnsmasq's interface listening behavior -i, interface Specify dnsmasq's interface listening behavior
Add '-h' for more info on interface usage" -l, privacylevel Set privacy level (0 = lowest, 3 = highest)"
exit 0 exit 0
} }
add_setting() { add_setting() {
echo "${1}=${2}" >> "${setupVars}" echo "${1}=${2}" >> "${setupVars}"
} }
delete_setting() { delete_setting() {
sed -i "/${1}/d" "${setupVars}" sed -i "/${1}/d" "${setupVars}"
} }
change_setting() { change_setting() {
delete_setting "${1}" delete_setting "${1}"
add_setting "${1}" "${2}" add_setting "${1}" "${2}"
}
addFTLsetting() {
echo "${1}=${2}" >> "${FTLconf}"
}
deleteFTLsetting() {
sed -i "/${1}/d" "${FTLconf}"
}
changeFTLsetting() {
deleteFTLsetting "${1}"
addFTLsetting "${1}" "${2}"
} }
add_dnsmasq_setting() { add_dnsmasq_setting() {
if [[ "${2}" != "" ]]; then if [[ "${2}" != "" ]]; then
echo "${1}=${2}" >> "${dnsmasqconfig}" echo "${1}=${2}" >> "${dnsmasqconfig}"
else else
echo "${1}" >> "${dnsmasqconfig}" echo "${1}" >> "${dnsmasqconfig}"
fi fi
} }
delete_dnsmasq_setting() { delete_dnsmasq_setting() {
sed -i "/${1}/d" "${dnsmasqconfig}" sed -i "/${1}/d" "${dnsmasqconfig}"
} }
SetTemperatureUnit() { SetTemperatureUnit() {
change_setting "TEMPERATUREUNIT" "${unit}" change_setting "TEMPERATUREUNIT" "${unit}"
echo -e " ${TICK} Set temperature unit to ${unit}" echo -e " ${TICK} Set temperature unit to ${unit}"
} }
HashPassword() { HashPassword() {
# Compute password hash twice to avoid rainbow table vulnerability # Compute password hash twice to avoid rainbow table vulnerability
return=$(echo -n ${1} | sha256sum | sed 's/\s.*$//') return=$(echo -n ${1} | sha256sum | sed 's/\s.*$//')
return=$(echo -n ${return} | sha256sum | sed 's/\s.*$//') return=$(echo -n ${return} | sha256sum | sed 's/\s.*$//')
echo ${return} echo ${return}
} }
SetWebPassword() { SetWebPassword() {
if [ "${SUDO_USER}" == "www-data" ]; then if [ "${SUDO_USER}" == "www-data" ]; then
echo "Security measure: user www-data is not allowed to change webUI password!" echo "Security measure: user www-data is not allowed to change webUI password!"
echo "Exiting" echo "Exiting"
exit 1 exit 1
fi fi
if [ "${SUDO_USER}" == "lighttpd" ]; then if [ "${SUDO_USER}" == "lighttpd" ]; then
echo "Security measure: user lighttpd is not allowed to change webUI password!" echo "Security measure: user lighttpd is not allowed to change webUI password!"
echo "Exiting" echo "Exiting"
exit 1 exit 1
fi fi
if (( ${#args[2]} > 0 )) ; then if (( ${#args[2]} > 0 )) ; then
readonly PASSWORD="${args[2]}" readonly PASSWORD="${args[2]}"
readonly CONFIRM="${PASSWORD}" readonly CONFIRM="${PASSWORD}"
else else
# Prevents a bug if the user presses Ctrl+C and it continues to hide the text typed. # 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 # 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 trap '{ echo -e "\nNo password will be set" ; stty sane ; exit 1; }' INT
read -s -p "Enter New Password (Blank for no password): " PASSWORD read -s -p "Enter New Password (Blank for no password): " PASSWORD
echo "" echo ""
if [ "${PASSWORD}" == "" ]; then if [ "${PASSWORD}" == "" ]; then
change_setting "WEBPASSWORD" "" change_setting "WEBPASSWORD" ""
echo -e " ${TICK} Password Removed" echo -e " ${TICK} Password Removed"
exit 0 exit 0
fi fi
read -s -p "Confirm Password: " CONFIRM read -s -p "Confirm Password: " CONFIRM
echo "" echo ""
fi fi
if [ "${PASSWORD}" == "${CONFIRM}" ] ; then if [ "${PASSWORD}" == "${CONFIRM}" ] ; then
hash=$(HashPassword "${PASSWORD}") hash=$(HashPassword "${PASSWORD}")
# Save hash to file # Save hash to file
change_setting "WEBPASSWORD" "${hash}" change_setting "WEBPASSWORD" "${hash}"
echo -e " ${TICK} New password set" echo -e " ${TICK} New password set"
else else
echo -e " ${CROSS} 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 exit 1
fi fi
} }
ProcessDNSSettings() { ProcessDNSSettings() {
source "${setupVars}" source "${setupVars}"
delete_dnsmasq_setting "server" delete_dnsmasq_setting "server"
COUNTER=1 COUNTER=1
while [[ 1 ]]; do while [[ 1 ]]; do
var=PIHOLE_DNS_${COUNTER} var=PIHOLE_DNS_${COUNTER}
if [ -z "${!var}" ]; then if [ -z "${!var}" ]; then
break; break;
fi fi
add_dnsmasq_setting "server" "${!var}" add_dnsmasq_setting "server" "${!var}"
let COUNTER=COUNTER+1 let COUNTER=COUNTER+1
done done
# The option LOCAL_DNS_PORT is deprecated
# We apply it once more, and then convert it into the current format
if [ ! -z "${LOCAL_DNS_PORT}" ]; then
add_dnsmasq_setting "server" "127.0.0.1#${LOCAL_DNS_PORT}"
add_setting "PIHOLE_DNS_${COUNTER}" "127.0.0.1#${LOCAL_DNS_PORT}"
delete_setting "LOCAL_DNS_PORT"
fi
delete_dnsmasq_setting "domain-needed" delete_dnsmasq_setting "domain-needed"
if [[ "${DNS_FQDN_REQUIRED}" == true ]]; then if [[ "${DNS_FQDN_REQUIRED}" == true ]]; then
add_dnsmasq_setting "domain-needed" add_dnsmasq_setting "domain-needed"
fi fi
delete_dnsmasq_setting "bogus-priv" delete_dnsmasq_setting "bogus-priv"
if [[ "${DNS_BOGUS_PRIV}" == true ]]; then if [[ "${DNS_BOGUS_PRIV}" == true ]]; then
add_dnsmasq_setting "bogus-priv" add_dnsmasq_setting "bogus-priv"
fi fi
delete_dnsmasq_setting "dnssec" delete_dnsmasq_setting "dnssec"
delete_dnsmasq_setting "trust-anchor=" delete_dnsmasq_setting "trust-anchor="
if [[ "${DNSSEC}" == true ]]; then if [[ "${DNSSEC}" == true ]]; then
echo "dnssec echo "dnssec
trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5 trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5
trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
" >> "${dnsmasqconfig}" " >> "${dnsmasqconfig}"
fi fi
delete_dnsmasq_setting "host-record" delete_dnsmasq_setting "host-record"
if [ ! -z "${HOSTRECORD}" ]; then if [ ! -z "${HOSTRECORD}" ]; then
add_dnsmasq_setting "host-record" "${HOSTRECORD}" add_dnsmasq_setting "host-record" "${HOSTRECORD}"
fi fi
# Setup interface listening behavior of dnsmasq # Setup interface listening behavior of dnsmasq
delete_dnsmasq_setting "interface" delete_dnsmasq_setting "interface"
delete_dnsmasq_setting "local-service" delete_dnsmasq_setting "local-service"
if [[ "${DNSMASQ_LISTENING}" == "all" ]]; then if [[ "${DNSMASQ_LISTENING}" == "all" ]]; then
# Listen on all interfaces, permit all origins # Listen on all interfaces, permit all origins
add_dnsmasq_setting "except-interface" "nonexisting" add_dnsmasq_setting "except-interface" "nonexisting"
elif [[ "${DNSMASQ_LISTENING}" == "local" ]]; then elif [[ "${DNSMASQ_LISTENING}" == "local" ]]; then
# Listen only on all interfaces, but only local subnets # Listen only on all interfaces, but only local subnets
add_dnsmasq_setting "local-service" add_dnsmasq_setting "local-service"
else else
# Listen only on one interface # Listen only on one interface
# Use eth0 as fallback interface if interface is missing in setupVars.conf # Use eth0 as fallback interface if interface is missing in setupVars.conf
if [ -z "${PIHOLE_INTERFACE}" ]; then if [ -z "${PIHOLE_INTERFACE}" ]; then
PIHOLE_INTERFACE="eth0" PIHOLE_INTERFACE="eth0"
fi fi
add_dnsmasq_setting "interface" "${PIHOLE_INTERFACE}" add_dnsmasq_setting "interface" "${PIHOLE_INTERFACE}"
fi fi
if [[ "${CONDITIONAL_FORWARDING}" == true ]]; then
add_dnsmasq_setting "server=/${CONDITIONAL_FORWARDING_DOMAIN}/${CONDITIONAL_FORWARDING_IP}"
add_dnsmasq_setting "server=/${CONDITIONAL_FORWARDING_REVERSE}/${CONDITIONAL_FORWARDING_IP}"
fi
if [[ "${CONDITIONAL_FORWARDING}" == true ]]; then
add_dnsmasq_setting "server=/${CONDITIONAL_FORWARDING_DOMAIN}/${CONDITIONAL_FORWARDING_IP}"
add_dnsmasq_setting "server=/${CONDITIONAL_FORWARDING_REVERSE}/${CONDITIONAL_FORWARDING_IP}"
fi
} }
SetDNSServers() { SetDNSServers() {
# Save setting to file # Save setting to file
delete_setting "PIHOLE_DNS" delete_setting "PIHOLE_DNS"
IFS=',' read -r -a array <<< "${args[2]}" IFS=',' read -r -a array <<< "${args[2]}"
for index in "${!array[@]}" for index in "${!array[@]}"
do do
add_setting "PIHOLE_DNS_$((index+1))" "${array[index]}" add_setting "PIHOLE_DNS_$((index+1))" "${array[index]}"
done done
if [[ "${args[3]}" == "domain-needed" ]]; then if [[ "${args[3]}" == "domain-needed" ]]; then
change_setting "DNS_FQDN_REQUIRED" "true" change_setting "DNS_FQDN_REQUIRED" "true"
else else
change_setting "DNS_FQDN_REQUIRED" "false" change_setting "DNS_FQDN_REQUIRED" "false"
fi fi
if [[ "${args[4]}" == "bogus-priv" ]]; then if [[ "${args[4]}" == "bogus-priv" ]]; then
change_setting "DNS_BOGUS_PRIV" "true" change_setting "DNS_BOGUS_PRIV" "true"
else else
change_setting "DNS_BOGUS_PRIV" "false" change_setting "DNS_BOGUS_PRIV" "false"
fi fi
if [[ "${args[5]}" == "dnssec" ]]; then if [[ "${args[5]}" == "dnssec" ]]; then
change_setting "DNSSEC" "true" change_setting "DNSSEC" "true"
else else
change_setting "DNSSEC" "false" change_setting "DNSSEC" "false"
fi fi
if [[ "${args[6]}" == "conditional_forwarding" ]]; then
change_setting "CONDITIONAL_FORWARDING" "true" if [[ "${args[6]}" == "conditional_forwarding" ]]; then
change_setting "CONDITIONAL_FORWARDING_IP" "${args[7]}" change_setting "CONDITIONAL_FORWARDING" "true"
change_setting "CONDITIONAL_FORWARDING_DOMAIN" "${args[8]}" change_setting "CONDITIONAL_FORWARDING_IP" "${args[7]}"
change_setting "CONDITIONAL_FORWARDING_REVERSE" "${args[9]}" change_setting "CONDITIONAL_FORWARDING_DOMAIN" "${args[8]}"
else change_setting "CONDITIONAL_FORWARDING_REVERSE" "${args[9]}"
change_setting "CONDITIONAL_FORWARDING" "false" else
delete_setting "CONDITIONAL_FORWARDING_IP" change_setting "CONDITIONAL_FORWARDING" "false"
delete_setting "CONDITIONAL_FORWARDING_DOMAIN" delete_setting "CONDITIONAL_FORWARDING_IP"
delete_setting "CONDITIONAL_FORWARDING_REVERSE" delete_setting "CONDITIONAL_FORWARDING_DOMAIN"
fi delete_setting "CONDITIONAL_FORWARDING_REVERSE"
fi
ProcessDNSSettings
ProcessDNSSettings
# Restart dnsmasq to load new configuration
RestartDNS # Restart dnsmasq to load new configuration
RestartDNS
} }
SetExcludeDomains() { SetExcludeDomains() {
change_setting "API_EXCLUDE_DOMAINS" "${args[2]}" change_setting "API_EXCLUDE_DOMAINS" "${args[2]}"
} }
SetExcludeClients() { SetExcludeClients() {
change_setting "API_EXCLUDE_CLIENTS" "${args[2]}" change_setting "API_EXCLUDE_CLIENTS" "${args[2]}"
} }
Poweroff(){ Poweroff(){
nohup bash -c "sleep 5; poweroff" &> /dev/null </dev/null & nohup bash -c "sleep 5; poweroff" &> /dev/null </dev/null &
} }
Reboot() { Reboot() {
nohup bash -c "sleep 5; reboot" &> /dev/null </dev/null & nohup bash -c "sleep 5; reboot" &> /dev/null </dev/null &
} }
RestartDNS() { RestartDNS() {
/usr/local/bin/pihole restartdns /usr/local/bin/pihole restartdns
} }
SetQueryLogOptions() { SetQueryLogOptions() {
change_setting "API_QUERY_LOG_SHOW" "${args[2]}" change_setting "API_QUERY_LOG_SHOW" "${args[2]}"
} }
ProcessDHCPSettings() { ProcessDHCPSettings() {
source "${setupVars}" source "${setupVars}"
if [[ "${DHCP_ACTIVE}" == "true" ]]; then if [[ "${DHCP_ACTIVE}" == "true" ]]; then
interface="${PIHOLE_INTERFACE}" interface="${PIHOLE_INTERFACE}"
# Use eth0 as fallback interface # Use eth0 as fallback interface
if [ -z ${interface} ]; then if [ -z ${interface} ]; then
interface="eth0" interface="eth0"
fi fi
if [[ "${PIHOLE_DOMAIN}" == "" ]]; then if [[ "${PIHOLE_DOMAIN}" == "" ]]; then
PIHOLE_DOMAIN="lan" PIHOLE_DOMAIN="lan"
change_setting "PIHOLE_DOMAIN" "${PIHOLE_DOMAIN}" change_setting "PIHOLE_DOMAIN" "${PIHOLE_DOMAIN}"
fi fi
if [[ "${DHCP_LEASETIME}" == "0" ]]; then if [[ "${DHCP_LEASETIME}" == "0" ]]; then
leasetime="infinite" leasetime="infinite"
elif [[ "${DHCP_LEASETIME}" == "" ]]; then elif [[ "${DHCP_LEASETIME}" == "" ]]; then
leasetime="24" leasetime="24"
change_setting "DHCP_LEASETIME" "${leasetime}" change_setting "DHCP_LEASETIME" "${leasetime}"
elif [[ "${DHCP_LEASETIME}" == "24h" ]]; then elif [[ "${DHCP_LEASETIME}" == "24h" ]]; then
#Installation is affected by known bug, introduced in a previous version. #Installation is affected by known bug, introduced in a previous version.
#This will automatically clean up setupVars.conf and remove the unnecessary "h" #This will automatically clean up setupVars.conf and remove the unnecessary "h"
leasetime="24" leasetime="24"
change_setting "DHCP_LEASETIME" "${leasetime}" change_setting "DHCP_LEASETIME" "${leasetime}"
else else
leasetime="${DHCP_LEASETIME}h" leasetime="${DHCP_LEASETIME}h"
fi fi
# Write settings to file # Write settings to file
@ -299,12 +322,12 @@ dhcp-leasefile=/etc/pihole/dhcp.leases
#quiet-dhcp #quiet-dhcp
" > "${dhcpconfig}" " > "${dhcpconfig}"
if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then
echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}"
fi fi
if [[ "${DHCP_IPv6}" == "true" ]]; then if [[ "${DHCP_IPv6}" == "true" ]]; then
echo "#quiet-dhcp6 echo "#quiet-dhcp6
#enable-ra #enable-ra
dhcp-option=option6:dns-server,[::] dhcp-option=option6:dns-server,[::]
dhcp-range=::100,::1ff,constructor:${interface},ra-names,slaac,${leasetime} dhcp-range=::100,::1ff,constructor:${interface},ra-names,slaac,${leasetime}
@ -312,158 +335,160 @@ ra-param=*,0,0
" >> "${dhcpconfig}" " >> "${dhcpconfig}"
fi fi
else else
if [[ -f "${dhcpconfig}" ]]; then if [[ -f "${dhcpconfig}" ]]; then
rm "${dhcpconfig}" &> /dev/null rm "${dhcpconfig}" &> /dev/null
fi fi
fi fi
} }
EnableDHCP() { EnableDHCP() {
change_setting "DHCP_ACTIVE" "true" change_setting "DHCP_ACTIVE" "true"
change_setting "DHCP_START" "${args[2]}" change_setting "DHCP_START" "${args[2]}"
change_setting "DHCP_END" "${args[3]}" change_setting "DHCP_END" "${args[3]}"
change_setting "DHCP_ROUTER" "${args[4]}" change_setting "DHCP_ROUTER" "${args[4]}"
change_setting "DHCP_LEASETIME" "${args[5]}" change_setting "DHCP_LEASETIME" "${args[5]}"
change_setting "PIHOLE_DOMAIN" "${args[6]}" change_setting "PIHOLE_DOMAIN" "${args[6]}"
change_setting "DHCP_IPv6" "${args[7]}" change_setting "DHCP_IPv6" "${args[7]}"
# Remove possible old setting from file # Remove possible old setting from file
delete_dnsmasq_setting "dhcp-" delete_dnsmasq_setting "dhcp-"
delete_dnsmasq_setting "quiet-dhcp" delete_dnsmasq_setting "quiet-dhcp"
ProcessDHCPSettings ProcessDHCPSettings
RestartDNS RestartDNS
} }
DisableDHCP() { DisableDHCP() {
change_setting "DHCP_ACTIVE" "false" change_setting "DHCP_ACTIVE" "false"
# Remove possible old setting from file # Remove possible old setting from file
delete_dnsmasq_setting "dhcp-" delete_dnsmasq_setting "dhcp-"
delete_dnsmasq_setting "quiet-dhcp" delete_dnsmasq_setting "quiet-dhcp"
ProcessDHCPSettings ProcessDHCPSettings
RestartDNS RestartDNS
} }
SetWebUILayout() { SetWebUILayout() {
change_setting "WEBUIBOXEDLAYOUT" "${args[2]}" change_setting "WEBUIBOXEDLAYOUT" "${args[2]}"
} }
CustomizeAdLists() { CustomizeAdLists() {
list="/etc/pihole/adlists.list" list="/etc/pihole/adlists.list"
if [[ "${args[2]}" == "enable" ]]; then if [[ "${args[2]}" == "enable" ]]; then
sed -i "\\@${args[3]}@s/^#http/http/g" "${list}" 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}" sed -i "\\@${args[3]}@s/^http/#http/g" "${list}"
elif [[ "${args[2]}" == "add" ]]; then elif [[ "${args[2]}" == "add" ]]; then
echo "${args[3]}" >> ${list} if [[ $(grep -c "^${args[3]}$" "${list}") -eq 0 ]] ; then
elif [[ "${args[2]}" == "del" ]]; then echo "${args[3]}" >> ${list}
var=$(echo "${args[3]}" | sed 's/\//\\\//g') fi
sed -i "/${var}/Id" "${list}" elif [[ "${args[2]}" == "del" ]]; then
else var=$(echo "${args[3]}" | sed 's/\//\\\//g')
echo "Not permitted" sed -i "/${var}/Id" "${list}"
return 1 else
fi echo "Not permitted"
return 1
fi
} }
SetPrivacyMode() { SetPrivacyMode() {
if [[ "${args[2]}" == "true" ]]; then if [[ "${args[2]}" == "true" ]]; then
change_setting "API_PRIVACY_MODE" "true" change_setting "API_PRIVACY_MODE" "true"
else else
change_setting "API_PRIVACY_MODE" "false" change_setting "API_PRIVACY_MODE" "false"
fi fi
} }
ResolutionSettings() { ResolutionSettings() {
typ="${args[2]}" typ="${args[2]}"
state="${args[3]}" state="${args[3]}"
if [[ "${typ}" == "forward" ]]; then if [[ "${typ}" == "forward" ]]; then
change_setting "API_GET_UPSTREAM_DNS_HOSTNAME" "${state}" change_setting "API_GET_UPSTREAM_DNS_HOSTNAME" "${state}"
elif [[ "${typ}" == "clients" ]]; then elif [[ "${typ}" == "clients" ]]; then
change_setting "API_GET_CLIENT_HOSTNAME" "${state}" change_setting "API_GET_CLIENT_HOSTNAME" "${state}"
fi fi
} }
AddDHCPStaticAddress() { AddDHCPStaticAddress() {
mac="${args[2]}" mac="${args[2]}"
ip="${args[3]}" ip="${args[3]}"
host="${args[4]}" host="${args[4]}"
if [[ "${ip}" == "noip" ]]; then if [[ "${ip}" == "noip" ]]; then
# Static host name # Static host name
echo "dhcp-host=${mac},${host}" >> "${dhcpstaticconfig}" echo "dhcp-host=${mac},${host}" >> "${dhcpstaticconfig}"
elif [[ "${host}" == "nohost" ]]; then elif [[ "${host}" == "nohost" ]]; then
# Static IP # Static IP
echo "dhcp-host=${mac},${ip}" >> "${dhcpstaticconfig}" echo "dhcp-host=${mac},${ip}" >> "${dhcpstaticconfig}"
else else
# Full info given # Full info given
echo "dhcp-host=${mac},${ip},${host}" >> "${dhcpstaticconfig}" echo "dhcp-host=${mac},${ip},${host}" >> "${dhcpstaticconfig}"
fi fi
} }
RemoveDHCPStaticAddress() { RemoveDHCPStaticAddress() {
mac="${args[2]}" mac="${args[2]}"
sed -i "/dhcp-host=${mac}.*/d" "${dhcpstaticconfig}" sed -i "/dhcp-host=${mac}.*/d" "${dhcpstaticconfig}"
} }
SetHostRecord() { SetHostRecord() {
if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then
echo "Usage: pihole -a hostrecord <domain> [IPv4-address],[IPv6-address] echo "Usage: pihole -a hostrecord <domain> [IPv4-address],[IPv6-address]
Example: 'pihole -a hostrecord home.domain.com 192.168.1.1,2001:db8:a0b:12f0::1' 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 Add a name to the DNS associated to an IPv4/IPv6 address
Options: Options:
\"\" Empty: Remove host record \"\" Empty: Remove host record
-h, --help Show this help dialog" -h, --help Show this help dialog"
exit 0 exit 0
fi fi
if [[ -n "${args[3]}" ]]; then if [[ -n "${args[3]}" ]]; then
change_setting "HOSTRECORD" "${args[2]},${args[3]}" change_setting "HOSTRECORD" "${args[2]},${args[3]}"
echo -e " ${TICK} Setting host record for ${args[2]} to ${args[3]}" echo -e " ${TICK} Setting host record for ${args[2]} to ${args[3]}"
else else
change_setting "HOSTRECORD" "" change_setting "HOSTRECORD" ""
echo -e " ${TICK} Removing host record" echo -e " ${TICK} Removing host record"
fi fi
ProcessDNSSettings ProcessDNSSettings
# Restart dnsmasq to load new configuration # Restart dnsmasq to load new configuration
RestartDNS RestartDNS
} }
SetAdminEmail() { SetAdminEmail() {
if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then
echo "Usage: pihole -a email <address> echo "Usage: pihole -a email <address>
Example: 'pihole -a email admin@address.com' Example: 'pihole -a email admin@address.com'
Set an administrative contact address for the Block Page Set an administrative contact address for the Block Page
Options: Options:
\"\" Empty: Remove admin contact \"\" Empty: Remove admin contact
-h, --help Show this help dialog" -h, --help Show this help dialog"
exit 0 exit 0
fi fi
if [[ -n "${args[2]}" ]]; then if [[ -n "${args[2]}" ]]; then
change_setting "ADMIN_EMAIL" "${args[2]}" change_setting "ADMIN_EMAIL" "${args[2]}"
echo -e " ${TICK} Setting admin contact to ${args[2]}" echo -e " ${TICK} Setting admin contact to ${args[2]}"
else else
change_setting "ADMIN_EMAIL" "" change_setting "ADMIN_EMAIL" ""
echo -e " ${TICK} Removing admin contact" echo -e " ${TICK} Removing admin contact"
fi fi
} }
SetListeningMode() { SetListeningMode() {
source "${setupVars}" source "${setupVars}"
if [[ "$3" == "-h" ]] || [[ "$3" == "--help" ]]; then if [[ "$3" == "-h" ]] || [[ "$3" == "--help" ]]; then
echo "Usage: pihole -a -i [interface] echo "Usage: pihole -a -i [interface]
Example: 'pihole -a -i local' Example: 'pihole -a -i local'
Specify dnsmasq's network interface listening behavior Specify dnsmasq's network interface listening behavior
@ -472,74 +497,82 @@ Interfaces:
devices that are at most one hop away (local devices) devices that are at most one hop away (local devices)
single Listen only on ${PIHOLE_INTERFACE} interface single Listen only on ${PIHOLE_INTERFACE} interface
all Listen on all interfaces, permit all origins" all Listen on all interfaces, permit all origins"
exit 0 exit 0
fi fi
if [[ "${args[2]}" == "all" ]]; then if [[ "${args[2]}" == "all" ]]; then
echo -e " ${INFO} Listening on all interfaces, permiting all origins. Please use a firewall!" echo -e " ${INFO} Listening on all interfaces, permiting all origins. Please use a firewall!"
change_setting "DNSMASQ_LISTENING" "all" change_setting "DNSMASQ_LISTENING" "all"
elif [[ "${args[2]}" == "local" ]]; then elif [[ "${args[2]}" == "local" ]]; then
echo -e " ${INFO} Listening on all interfaces, permiting origins from one hop away (LAN)" echo -e " ${INFO} Listening on all interfaces, permiting origins from one hop away (LAN)"
change_setting "DNSMASQ_LISTENING" "local" change_setting "DNSMASQ_LISTENING" "local"
else else
echo -e " ${INFO} Listening only on interface ${PIHOLE_INTERFACE}" echo -e " ${INFO} Listening only on interface ${PIHOLE_INTERFACE}"
change_setting "DNSMASQ_LISTENING" "single" change_setting "DNSMASQ_LISTENING" "single"
fi fi
# Don't restart DNS server yet because other settings # Don't restart DNS server yet because other settings
# will be applied afterwards if "-web" is set # will be applied afterwards if "-web" is set
if [[ "${args[3]}" != "-web" ]]; then if [[ "${args[3]}" != "-web" ]]; then
ProcessDNSSettings ProcessDNSSettings
# Restart dnsmasq to load new configuration # Restart dnsmasq to load new configuration
RestartDNS RestartDNS
fi fi
} }
Teleporter() { Teleporter() {
local datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S") 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" php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "pi-hole-teleporter_${datetimestamp}.zip"
} }
audit() audit()
{ {
echo "${args[2]}" >> /etc/pihole/auditlog.list echo "${args[2]}" >> /etc/pihole/auditlog.list
}
SetPrivacyLevel() {
# Set privacy level. Minimum is 0, maximum is 3
if [ "${args[2]}" -ge 0 ] && [ "${args[2]}" -le 3 ]; then
changeFTLsetting "PRIVACYLEVEL" "${args[2]}"
fi
} }
main() { main() {
args=("$@") args=("$@")
case "${args[1]}" in case "${args[1]}" in
"-p" | "password" ) SetWebPassword;; "-p" | "password" ) SetWebPassword;;
"-c" | "celsius" ) unit="C"; SetTemperatureUnit;; "-c" | "celsius" ) unit="C"; SetTemperatureUnit;;
"-f" | "fahrenheit" ) unit="F"; SetTemperatureUnit;; "-f" | "fahrenheit" ) unit="F"; SetTemperatureUnit;;
"-k" | "kelvin" ) unit="K"; SetTemperatureUnit;; "-k" | "kelvin" ) unit="K"; SetTemperatureUnit;;
"setdns" ) SetDNSServers;; "setdns" ) SetDNSServers;;
"setexcludedomains" ) SetExcludeDomains;; "setexcludedomains" ) SetExcludeDomains;;
"setexcludeclients" ) SetExcludeClients;; "setexcludeclients" ) SetExcludeClients;;
"poweroff" ) Poweroff;; "poweroff" ) Poweroff;;
"reboot" ) Reboot;; "reboot" ) Reboot;;
"restartdns" ) RestartDNS;; "restartdns" ) RestartDNS;;
"setquerylog" ) SetQueryLogOptions;; "setquerylog" ) SetQueryLogOptions;;
"enabledhcp" ) EnableDHCP;; "enabledhcp" ) EnableDHCP;;
"disabledhcp" ) DisableDHCP;; "disabledhcp" ) DisableDHCP;;
"layout" ) SetWebUILayout;; "layout" ) SetWebUILayout;;
"-h" | "--help" ) helpFunc;; "-h" | "--help" ) helpFunc;;
"privacymode" ) SetPrivacyMode;; "privacymode" ) SetPrivacyMode;;
"resolve" ) ResolutionSettings;; "resolve" ) ResolutionSettings;;
"addstaticdhcp" ) AddDHCPStaticAddress;; "addstaticdhcp" ) AddDHCPStaticAddress;;
"removestaticdhcp" ) RemoveDHCPStaticAddress;; "removestaticdhcp" ) RemoveDHCPStaticAddress;;
"-r" | "hostrecord" ) SetHostRecord "$3";; "-r" | "hostrecord" ) SetHostRecord "$3";;
"-e" | "email" ) SetAdminEmail "$3";; "-e" | "email" ) SetAdminEmail "$3";;
"-i" | "interface" ) SetListeningMode "$@";; "-i" | "interface" ) SetListeningMode "$@";;
"-t" | "teleporter" ) Teleporter;; "-t" | "teleporter" ) Teleporter;;
"adlist" ) CustomizeAdLists;; "adlist" ) CustomizeAdLists;;
"audit" ) audit;; "audit" ) audit;;
* ) helpFunc;; "-l" | "privacylevel" ) SetPrivacyLevel;;
esac * ) helpFunc;;
esac
shift
shift
if [[ $# = 0 ]]; then
helpFunc if [[ $# = 0 ]]; then
fi helpFunc
fi
} }

@ -0,0 +1,28 @@
#!/bin/bash
# Pi-hole: A black hole for Internet advertisements
# (c) 2017 Pi-hole, LLC (https://pi-hole.net)
# Network-wide ad blocking via your own hardware.
#
# Provides an automated migration subroutine to convert Pi-hole v3.x wildcard domains to Pi-hole v4.x regex filters
#
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
# regexFile set in gravity.sh
wildcardFile="/etc/dnsmasq.d/03-pihole-wildcard.conf"
convert_wildcard_to_regex() {
if [ ! -f "${wildcardFile}" ]; then
return
fi
local addrlines domains uniquedomains
# Obtain wildcard domains from old file
addrlines="$(grep -oE "/.*/" ${wildcardFile})"
# Strip "/" from domain names and convert "." to regex-compatible "\."
domains="$(sed 's/\///g;s/\./\\./g' <<< "${addrlines}")"
# Remove repeated domains (may have been inserted two times due to A and AAAA blocking)
uniquedomains="$(uniq <<< "${domains}")"
# Automatically generate regex filters and remove old wildcards file
awk '{print "(^|\\.)"$0"$"}' <<< "${uniquedomains}" >> "${regexFile:?}" && rm "${wildcardFile}"
}

@ -0,0 +1,84 @@
### This file contains parameters for FTL behavior.
### At install, all parameters are commented out. The user can select desired options.
### Options shown are the default configuration. No modification is needed for most
### installations.
### Visit https://docs.pi-hole.net/ftldns/configfile/ for more detailed parameter explanations
## Socket Listening
## Listen only for local socket connections or permit all connections
## Options: localonly, all
#SOCKET_LISTENING=localonly
## Query Display
## Display all queries? Set to no to hide query display
## Options: yes, no
#QUERY_DISPLAY=yes
## AAA Query Analysis
## Allow FTL to analyze AAAA queries from pihole.log?
## Options: yes, no
#AAAA_QUERY_ANALYSIS=yes
## Resolve IPv6
## Should FTL try to resolve IPv6 addresses to host names?
## Options: yes, no
#RESOLVE_IPV6=yes
## Resolve IPv4
## Should FTL try to resolve IPv4 addresses to host names?
## Options: yes, no
#RESOLVE_IPV4=yes
## Max Database Days
## How long should queries be stored in the database (days)?
## Setting this to 0 disables the database
## See: https://docs.pi-hole.net/ftldns/database/
## Options: number of days
#MAXDBDAYS=365
## Database Interval
## How often do we store queries in FTL's database (minutes)?
## See: https://docs.pi-hole.net/ftldns/database/
## Options: number of minutes
#DBINTERVAL=1.0
## Database File
## Specify path and filename of FTL's SQLite3 long-term database.
## Setting this to DBFILE= disables the database altogether
## See: https://docs.pi-hole.net/ftldns/database/
## Option: path to db file
#DBFILE=/etc/pihole/pihole-FTL.db
## Max Log Age
## Up to how many hours of queries should be imported from the database and logs (hours)?
## Maximum is 744 (31 days)
## Options: number of days
#MAXLOGAGE=24.0
## FTL Port
## On which port should FTL be listening?
## Options: tcp port
#FTLPORT=4711
## Privacy Level
## Which privacy level is used?
## See: https://docs.pi-hole.net/ftldns/privacylevels/
## Options: 0, 1, 2, 3
#PRIVACYLEVEL=0
## Ignore Localhost
## Should FTL ignore queries coming from the local machine?
## Options: yes, no
#IGNORE_LOCALHOST=no
## Blocking Mode
## How should FTL reply to blocked queries?
## See: https://docs.pi-hole.net/ftldns/blockingmode/
## Options: NULL, IP-AAAA-NODATA, IP, NXDOMAIN
#BLOCKINGMODE=NULL
## Regex Debug Mode
## Controls if FTLDNS should print extended details about regex matching into pihole-FTL.log.
## See: https://docs.pi-hole.net/ftldns/regex/overview/
## Options: true, false
#REGEX_DEBUGMODE=false

@ -20,6 +20,7 @@ is_running() {
ps "$(get_pid)" > /dev/null 2>&1 ps "$(get_pid)" > /dev/null 2>&1
} }
# Start the service # Start the service
start() { start() {
if is_running; then if is_running; then
@ -29,9 +30,12 @@ start() {
mkdir -p /var/run/pihole mkdir -p /var/run/pihole
mkdir -p /var/log/pihole mkdir -p /var/log/pihole
chown pihole:pihole /var/run/pihole /var/log/pihole chown pihole:pihole /var/run/pihole /var/log/pihole
rm /var/run/pihole/FTL.sock rm /var/run/pihole/FTL.sock 2> /dev/null
chown pihole:pihole /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /etc/pihole chown pihole:pihole /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port
chown pihole:pihole /etc/pihole /etc/pihole/dhcp.leases /var/log/pihole.log
chmod 0644 /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /var/log/pihole.log chmod 0644 /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /var/log/pihole.log
setcap CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN+eip "$(which pihole-FTL)"
echo "nameserver 127.0.0.1" | /sbin/resolvconf -a lo.piholeFTL
su -s /bin/sh -c "/usr/bin/pihole-FTL" "$FTLUSER" su -s /bin/sh -c "/usr/bin/pihole-FTL" "$FTLUSER"
echo echo
fi fi
@ -40,6 +44,7 @@ start() {
# Stop the service # Stop the service
stop() { stop() {
if is_running; then if is_running; then
/sbin/resolvconf -d lo.piholeFTL
kill "$(get_pid)" kill "$(get_pid)"
for i in {1..5}; do for i in {1..5}; do
if ! is_running; then if ! is_running; then
@ -64,13 +69,25 @@ stop() {
echo echo
} }
# Indicate the service status
status() {
if is_running; then
echo "[ ok ] pihole-FTL is running"
exit 0
else
echo "[ ] pihole-FTL is not running"
exit 1
fi
}
### main logic ### ### main logic ###
case "$1" in case "$1" in
stop) stop)
stop stop
;; ;;
status) status)
status pihole-FTL status
;; ;;
start|restart|reload|condrestart) start|restart|reload|condrestart)
stop stop

@ -1,11 +1,79 @@
_pihole() { _pihole() {
local cur prev opts local cur prev opts opts_admin opts_checkout opts_chronometer opts_debug opts_interface opts_logging opts_privacy opts_query opts_update opts_version
COMPREPLY=() COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="admin blacklist chronometer debug disable enable flush help logging query reconfigure restartdns setupLCD status tail uninstall updateGravity updatePihole version whitelist checkout" prev2="${COMP_WORDS[COMP_CWORD-2]}"
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) case "${prev}" in
"pihole")
opts="admin blacklist checkout chronometer debug disable enable flush help logging query reconfigure regex restartdns status tail uninstall updateGravity updatePihole version wildcard whitelist"
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
;;
"whitelist"|"blacklist"|"wildcard"|"regex")
opts_lists="\--delmode \--noreload \--quiet \--list \--nuke"
COMPREPLY=( $(compgen -W "${opts_lists}" -- ${cur}) )
;;
"admin")
opts_admin="celsius email fahrenheit hostrecord interface kelvin password privacylevel"
COMPREPLY=( $(compgen -W "${opts_admin}" -- ${cur}) )
;;
"checkout")
opts_checkout="core ftl web master dev"
COMPREPLY=( $(compgen -W "${opts_checkout}" -- ${cur}) )
;;
"chronometer")
opts_chronometer="\--exit \--json \--refresh"
COMPREPLY=( $(compgen -W "${opts_chronometer}" -- ${cur}) )
;;
"debug")
opts_debug="-a"
COMPREPLY=( $(compgen -W "${opts_debug}" -- ${cur}) )
;;
"logging")
opts_logging="on off 'off noflush'"
COMPREPLY=( $(compgen -W "${opts_logging}" -- ${cur}) )
;;
"query")
opts_query="-adlist -all -exact"
COMPREPLY=( $(compgen -W "${opts_query}" -- ${cur}) )
;;
"updatePihole"|"-up")
opts_update="--check-only"
COMPREPLY=( $(compgen -W "${opts_update}" -- ${cur}) )
;;
"version")
opts_version="\--admin \--current \--ftl \--hash \--latest \--pihole"
COMPREPLY=( $(compgen -W "${opts_version}" -- ${cur}) )
;;
"interface")
if ( [[ "$prev2" == "admin" ]] || [[ "$prev2" == "-a" ]] ); then
opts_interface="$(cat /proc/net/dev | cut -d: -s -f1)"
COMPREPLY=( $(compgen -W "${opts_interface}" -- ${cur}) )
else
return 1
fi
;;
"privacylevel")
if ( [[ "$prev2" == "admin" ]] || [[ "$prev2" == "-a" ]] ); then
opts_privacy="0 1 2 3"
COMPREPLY=( $(compgen -W "${opts_privacy}" -- ${cur}) )
else
return 1
fi
;;
"core"|"admin"|"ftl")
if [[ "$prev2" == "checkout" ]]; then
opts_checkout="master dev"
COMPREPLY=( $(compgen -W "${opts_checkout}" -- ${cur}) )
else
return 1
fi
;;
*)
return 1
;;
esac
return 0 return 0
} }
complete -F _pihole pihole complete -F _pihole pihole

@ -64,7 +64,7 @@ if ($serverName === "pi.hole") {
<html><head> <html><head>
$viewPort $viewPort
<link rel='stylesheet' href='/pihole/blockingpage.css' type='text/css'/> <link rel='stylesheet' href='/pihole/blockingpage.css' type='text/css'/>
</head><body id='splashpage'><img src='/admin/img/logo.svg'/><br/>Pi-<b>hole</b>: Your black hole for Internet advertisements</body></html> </head><body id='splashpage'><img src='/admin/img/logo.svg'/><br/>Pi-<b>hole</b>: Your black hole for Internet advertisements<br><a href='/admin'>Did you mean to go to the admin panel?</a></body></html>
"; ";
// Set splash/landing page based off presence of $landPage // Set splash/landing page based off presence of $landPage
@ -102,8 +102,10 @@ if ($serverName === "pi.hole") {
$bpAskAdmin = !empty($svEmail) ? '<a href="mailto:'.$svEmail.'?subject=Site Blocked: '.$serverName.'"></a>' : "<span/>"; $bpAskAdmin = !empty($svEmail) ? '<a href="mailto:'.$svEmail.'?subject=Site Blocked: '.$serverName.'"></a>' : "<span/>";
// Determine if at least one block list has been generated // Determine if at least one block list has been generated
if (empty(glob("/etc/pihole/list.0.*.domains"))) $blocklistglob = glob("/etc/pihole/list.0.*.domains");
if ($blocklistglob === array()) {
die("[ERROR] There are no domain lists generated lists within <code>/etc/pihole/</code>! Please update gravity by running <code>pihole -g</code>, or repair Pi-hole using <code>pihole -r</code>."); die("[ERROR] There are no domain lists generated lists within <code>/etc/pihole/</code>! Please update gravity by running <code>pihole -g</code>, or repair Pi-hole using <code>pihole -r</code>.");
}
// Set location of adlists file // Set location of adlists file
if (is_file("/etc/pihole/adlists.list")) { if (is_file("/etc/pihole/adlists.list")) {
@ -327,6 +329,7 @@ setHeader();
setTimeout(function(){window.location.reload(1);}, 10000); setTimeout(function(){window.location.reload(1);}, 10000);
$("#bpOutput").removeClass("add"); $("#bpOutput").removeClass("add");
$("#bpOutput").addClass("success"); $("#bpOutput").addClass("success");
$("#bpOutput").html("");
} else { } else {
$("#bpOutput").removeClass("add"); $("#bpOutput").removeClass("add");
$("#bpOutput").addClass("error"); $("#bpOutput").addClass("error");
@ -336,6 +339,7 @@ setHeader();
error: function(jqXHR, exception) { error: function(jqXHR, exception) {
$("#bpOutput").removeClass("add"); $("#bpOutput").removeClass("add");
$("#bpOutput").addClass("exception"); $("#bpOutput").addClass("exception");
$("#bpOutput").html("");
} }
}); });
} }

File diff suppressed because it is too large Load Diff

@ -11,29 +11,29 @@
source "/opt/pihole/COL_TABLE" source "/opt/pihole/COL_TABLE"
while true; do while true; do
read -rp " ${QST} Are you sure you would like to remove ${COL_WHITE}Pi-hole${COL_NC}? [y/N] " yn read -rp " ${QST} Are you sure you would like to remove ${COL_WHITE}Pi-hole${COL_NC}? [y/N] " yn
case ${yn} in case ${yn} in
[Yy]* ) break;; [Yy]* ) break;;
[Nn]* ) echo -e "\n ${COL_LIGHT_GREEN}Uninstall has been cancelled${COL_NC}"; exit 0;; [Nn]* ) echo -e "${OVER} ${COL_LIGHT_GREEN}Uninstall has been cancelled${COL_NC}"; exit 0;;
* ) echo -e "\n ${COL_LIGHT_GREEN}Uninstall has been cancelled${COL_NC}"; exit 0;; * ) echo -e "${OVER} ${COL_LIGHT_GREEN}Uninstall has been cancelled${COL_NC}"; exit 0;;
esac esac
done done
# Must be root to uninstall # Must be root to uninstall
str="Root user check" str="Root user check"
if [[ ${EUID} -eq 0 ]]; then if [[ ${EUID} -eq 0 ]]; then
echo -e " ${TICK} ${str}" echo -e " ${TICK} ${str}"
else else
# Check if sudo is actually installed # Check if sudo is actually installed
# If it isn't, exit because the uninstall can not complete # If it isn't, exit because the uninstall can not complete
if [ -x "$(command -v sudo)" ]; then if [ -x "$(command -v sudo)" ]; then
export SUDO="sudo" export SUDO="sudo"
else else
echo -e " ${CROSS} ${str} echo -e " ${CROSS} ${str}
Script called with non-root privileges Script called with non-root privileges
The Pi-hole requires elevated privleges to uninstall" The Pi-hole requires elevated privleges to uninstall"
exit 1 exit 1
fi fi
fi fi
readonly PI_HOLE_FILES_DIR="/etc/.pihole" readonly PI_HOLE_FILES_DIR="/etc/.pihole"
@ -46,178 +46,172 @@ source "${setupVars}"
distro_check distro_check
# Install packages used by the Pi-hole # Install packages used by the Pi-hole
if [[ "${INSTALL_WEB}" == true ]]; then DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}")
# Install the Web dependencies if [[ "${INSTALL_WEB_SERVER}" == true ]]; then
DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}" "${PIHOLE_WEB_DEPS[@]}") # Install the Web dependencies
# Otherwise, DEPS+=("${PIHOLE_WEB_DEPS[@]}")
else
# just install the Core dependencies
DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}")
fi fi
# Compatability # Compatability
if [ -x "$(command -v rpm)" ]; then if [ -x "$(command -v apt-get)" ]; then
# Fedora Family # Debian Family
PKG_REMOVE="${PKG_MANAGER} remove -y" PKG_REMOVE="${PKG_MANAGER} -y remove --purge"
package_check() { package_check() {
rpm -qa | grep ^$1- > /dev/null dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -c "ok installed"
} }
package_cleanup() { elif [ -x "$(command -v rpm)" ]; then
${SUDO} ${PKG_MANAGER} -y autoremove # Fedora Family
} PKG_REMOVE="${PKG_MANAGER} remove -y"
elif [ -x "$(command -v apt-get)" ]; then package_check() {
# Debian Family rpm -qa | grep "^$1-" > /dev/null
PKG_REMOVE="${PKG_MANAGER} -y remove --purge" }
package_check() {
dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -c "ok installed"
}
package_cleanup() {
${SUDO} ${PKG_MANAGER} -y autoremove
${SUDO} ${PKG_MANAGER} -y autoclean
}
else else
echo -e " ${CROSS} OS distribution not supported" echo -e " ${CROSS} OS distribution not supported"
exit 1 exit 1
fi fi
removeAndPurge() { removeAndPurge() {
# Purge dependencies # Purge dependencies
echo "" echo ""
for i in "${DEPS[@]}"; do for i in "${DEPS[@]}"; do
package_check ${i} > /dev/null if package_check "${i}" > /dev/null; then
if [[ "$?" -eq 0 ]]; then while true; do
while true; do read -rp " ${QST} Do you wish to remove ${COL_WHITE}${i}${COL_NC} from your system? [Y/N] " yn
read -rp " ${QST} Do you wish to remove ${COL_WHITE}${i}${COL_NC} from your system? [Y/N] " yn case ${yn} in
case ${yn} in [Yy]* )
[Yy]* ) echo -ne " ${INFO} Removing ${i}...";
echo -ne " ${INFO} Removing ${i}..."; ${SUDO} "${PKG_REMOVE} ${i}" &> /dev/null;
${SUDO} ${PKG_REMOVE} "${i}" &> /dev/null; echo -e "${OVER} ${INFO} Removed ${i}";
echo -e "${OVER} ${INFO} Removed ${i}"; break;;
break;; [Nn]* ) echo -e " ${INFO} Skipped ${i}"; break;;
[Nn]* ) echo -e " ${INFO} Skipped ${i}"; break;; esac
esac done
done else
else echo -e " ${INFO} Package ${i} not installed"
echo -e " ${INFO} Package ${i} not installed" fi
fi done
done
# Remove dnsmasq config files
# Remove dnsmasq config files ${SUDO} rm -f /etc/dnsmasq.conf /etc/dnsmasq.conf.orig /etc/dnsmasq.d/*-pihole*.conf &> /dev/null
${SUDO} rm -f /etc/dnsmasq.conf /etc/dnsmasq.conf.orig /etc/dnsmasq.d/01-pihole.conf &> /dev/null echo -e " ${TICK} Removing dnsmasq config files"
echo -e " ${TICK} Removing dnsmasq config files"
# Call removeNoPurge to remove Pi-hole specific files
# Take care of any additional package cleaning removeNoPurge
echo -ne " ${INFO} Removing & cleaning remaining dependencies..."
package_cleanup &> /dev/null
echo -e "${OVER} ${TICK} Removed & cleaned up remaining dependencies"
# Call removeNoPurge to remove Pi-hole specific files
removeNoPurge
} }
removeNoPurge() { removeNoPurge() {
# Only web directories/files that are created by Pi-hole should be removed # Only web directories/files that are created by Pi-hole should be removed
echo -ne " ${INFO} Removing Web Interface..." echo -ne " ${INFO} Removing Web Interface..."
${SUDO} rm -rf /var/www/html/admin &> /dev/null ${SUDO} rm -rf /var/www/html/admin &> /dev/null
${SUDO} rm -rf /var/www/html/pihole &> /dev/null ${SUDO} rm -rf /var/www/html/pihole &> /dev/null
${SUDO} rm -f /var/www/html/index.lighttpd.orig &> /dev/null ${SUDO} rm -f /var/www/html/index.lighttpd.orig &> /dev/null
# If the web directory is empty after removing these files, then the parent html folder can be removed. # If the web directory is empty after removing these files, then the parent html folder can be removed.
if [ -d "/var/www/html" ]; then if [ -d "/var/www/html" ]; then
if [[ ! "$(ls -A /var/www/html)" ]]; then if [[ ! "$(ls -A /var/www/html)" ]]; then
${SUDO} rm -rf /var/www/html &> /dev/null ${SUDO} rm -rf /var/www/html &> /dev/null
fi fi
fi fi
echo -e "${OVER} ${TICK} Removed Web Interface" echo -e "${OVER} ${TICK} Removed Web Interface"
# Attempt to preserve backwards compatibility with older versions # Attempt to preserve backwards compatibility with older versions
# to guarantee no additional changes were made to /etc/crontab after # to guarantee no additional changes were made to /etc/crontab after
# the installation of pihole, /etc/crontab.pihole should be permanently # the installation of pihole, /etc/crontab.pihole should be permanently
# preserved. # preserved.
if [[ -f /etc/crontab.orig ]]; then if [[ -f /etc/crontab.orig ]]; then
${SUDO} mv /etc/crontab /etc/crontab.pihole ${SUDO} mv /etc/crontab /etc/crontab.pihole
${SUDO} mv /etc/crontab.orig /etc/crontab ${SUDO} mv /etc/crontab.orig /etc/crontab
${SUDO} service cron restart ${SUDO} service cron restart
echo -e " ${TICK} Restored the default system cron" echo -e " ${TICK} Restored the default system cron"
fi
# Attempt to preserve backwards compatibility with older versions
if [[ -f /etc/cron.d/pihole ]];then
${SUDO} rm -f /etc/cron.d/pihole &> /dev/null
echo -e " ${TICK} Removed /etc/cron.d/pihole"
fi
package_check lighttpd > /dev/null
if [[ $? -eq 1 ]]; then
${SUDO} rm -rf /etc/lighttpd/ &> /dev/null
echo -e " ${TICK} Removed lighttpd"
else
if [ -f /etc/lighttpd/lighttpd.conf.orig ]; then
${SUDO} mv /etc/lighttpd/lighttpd.conf.orig /etc/lighttpd/lighttpd.conf
fi
fi
${SUDO} rm -f /etc/dnsmasq.d/adList.conf &> /dev/null
${SUDO} rm -f /etc/dnsmasq.d/01-pihole.conf &> /dev/null
${SUDO} rm -rf /var/log/*pihole* &> /dev/null
${SUDO} rm -rf /etc/pihole/ &> /dev/null
${SUDO} rm -rf /etc/.pihole/ &> /dev/null
${SUDO} rm -rf /opt/pihole/ &> /dev/null
${SUDO} rm -f /usr/local/bin/pihole &> /dev/null
${SUDO} rm -f /etc/bash_completion.d/pihole &> /dev/null
${SUDO} rm -f /etc/sudoers.d/pihole &> /dev/null
echo -e " ${TICK} Removed config files"
# Remove FTL
if command -v pihole-FTL &> /dev/null; then
echo -ne " ${INFO} Removing pihole-FTL..."
if [[ -x "$(command -v systemctl)" ]]; then
systemctl stop pihole-FTL
else
service pihole-FTL stop
fi fi
${SUDO} rm -f /etc/init.d/pihole-FTL # Attempt to preserve backwards compatibility with older versions
${SUDO} rm -f /usr/bin/pihole-FTL if [[ -f /etc/cron.d/pihole ]];then
echo -e "${OVER} ${TICK} Removed pihole-FTL" ${SUDO} rm -f /etc/cron.d/pihole &> /dev/null
fi echo -e " ${TICK} Removed /etc/cron.d/pihole"
fi
# If the pihole user exists, then remove package_check lighttpd > /dev/null
if id "pihole" &> /dev/null; then if [[ $? -eq 1 ]]; then
${SUDO} userdel -r pihole 2> /dev/null ${SUDO} rm -rf /etc/lighttpd/ &> /dev/null
if [[ "$?" -eq 0 ]]; then echo -e " ${TICK} Removed lighttpd"
echo -e " ${TICK} Removed 'pihole' user"
else else
echo -e " ${CROSS} Unable to remove 'pihole' user" if [ -f /etc/lighttpd/lighttpd.conf.orig ]; then
${SUDO} mv /etc/lighttpd/lighttpd.conf.orig /etc/lighttpd/lighttpd.conf
fi
fi
${SUDO} rm -f /etc/dnsmasq.d/adList.conf &> /dev/null
${SUDO} rm -f /etc/dnsmasq.d/01-pihole.conf &> /dev/null
${SUDO} rm -rf /var/log/*pihole* &> /dev/null
${SUDO} rm -rf /etc/pihole/ &> /dev/null
${SUDO} rm -rf /etc/.pihole/ &> /dev/null
${SUDO} rm -rf /opt/pihole/ &> /dev/null
${SUDO} rm -f /usr/local/bin/pihole &> /dev/null
${SUDO} rm -f /etc/bash_completion.d/pihole &> /dev/null
${SUDO} rm -f /etc/sudoers.d/pihole &> /dev/null
echo -e " ${TICK} Removed config files"
# Restore Resolved
if [[ -e /etc/systemd/resolved.conf.orig ]]; then
${SUDO} cp /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf
systemctl reload-or-restart systemd-resolved
fi
# Remove FTL
if command -v pihole-FTL &> /dev/null; then
echo -ne " ${INFO} Removing pihole-FTL..."
if [[ -x "$(command -v systemctl)" ]]; then
systemctl stop pihole-FTL
else
service pihole-FTL stop
fi
${SUDO} rm -f /etc/init.d/pihole-FTL
${SUDO} rm -f /usr/bin/pihole-FTL
echo -e "${OVER} ${TICK} Removed pihole-FTL"
fi
# If the pihole manpage exists, then delete and rebuild man-db
if [[ -f /usr/local/share/man/man8/pihole.8 ]]; then
${SUDO} rm -f /usr/local/share/man/man8/pihole.8 /usr/local/share/man/man8/pihole-FTL.8 /usr/local/share/man/man5/pihole-FTL.conf.5
${SUDO} mandb -q &>/dev/null
echo -e " ${TICK} Removed pihole man page"
fi
# If the pihole user exists, then remove
if id "pihole" &> /dev/null; then
if ${SUDO} userdel -r pihole 2> /dev/null; then
echo -e " ${TICK} Removed 'pihole' user"
else
echo -e " ${CROSS} Unable to remove 'pihole' user"
fi
fi fi
fi
echo -e "\n We're sorry to see you go, but thanks for checking out Pi-hole! echo -e "\\n We're sorry to see you go, but thanks for checking out Pi-hole!
If you need help, reach out to us on Github, Discourse, Reddit or Twitter If you need help, reach out to us on Github, Discourse, Reddit or Twitter
Reinstall at any time: ${COL_WHITE}curl -sSL https://install.pi-hole.net | bash${COL_NC} Reinstall at any time: ${COL_WHITE}curl -sSL https://install.pi-hole.net | bash${COL_NC}
${COL_LIGHT_RED}Please reset the DNS on your router/clients to restore internet connectivity ${COL_LIGHT_RED}Please reset the DNS on your router/clients to restore internet connectivity
${COL_LIGHT_GREEN}Uninstallation Complete! ${COL_NC}" ${COL_LIGHT_GREEN}Uninstallation Complete! ${COL_NC}"
} }
######### SCRIPT ########### ######### SCRIPT ###########
if command -v vcgencmd &> /dev/null; then if command -v vcgencmd &> /dev/null; then
echo -e " ${INFO} All dependencies are safe to remove on Raspbian" echo -e " ${INFO} All dependencies are safe to remove on Raspbian"
else else
echo -e " ${INFO} Be sure to confirm if any dependencies should not be removed" echo -e " ${INFO} Be sure to confirm if any dependencies should not be removed"
fi fi
while true; do while true; do
echo -e " ${INFO} ${COL_YELLOW}The following dependencies may have been added by the Pi-hole install:" echo -e " ${INFO} ${COL_YELLOW}The following dependencies may have been added by the Pi-hole install:"
echo -n " " echo -n " "
for i in "${DEPS[@]}"; do for i in "${DEPS[@]}"; do
echo -n "${i} " echo -n "${i} "
done done
echo "${COL_NC}" echo "${COL_NC}"
read -rp " ${QST} Do you wish to go through each dependency for removal? (Choosing No will leave all dependencies installed) [Y/n] " yn read -rp " ${QST} Do you wish to go through each dependency for removal? (Choosing No will leave all dependencies installed) [Y/n] " yn
case ${yn} in case ${yn} in
[Yy]* ) removeAndPurge; break;; [Yy]* ) removeAndPurge; break;;
[Nn]* ) removeNoPurge; break;; [Nn]* ) removeNoPurge; break;;
* ) removeAndPurge; break;; * ) removeAndPurge; break;;
esac esac
done done

@ -15,20 +15,20 @@ export LC_ALL=C
coltable="/opt/pihole/COL_TABLE" coltable="/opt/pihole/COL_TABLE"
source "${coltable}" source "${coltable}"
regexconverter="/opt/pihole/wildcard_regex_converter.sh"
source "${regexconverter}"
basename="pihole" basename="pihole"
PIHOLE_COMMAND="/usr/local/bin/${basename}" PIHOLE_COMMAND="/usr/local/bin/${basename}"
piholeDir="/etc/${basename}" piholeDir="/etc/${basename}"
piholeRepo="/etc/.${basename}"
adListFile="${piholeDir}/adlists.list" adListFile="${piholeDir}/adlists.list"
adListDefault="${piholeDir}/adlists.default" adListDefault="${piholeDir}/adlists.default"
adListRepoDefault="${piholeRepo}/adlists.default"
whitelistFile="${piholeDir}/whitelist.txt" whitelistFile="${piholeDir}/whitelist.txt"
blacklistFile="${piholeDir}/blacklist.txt" blacklistFile="${piholeDir}/blacklist.txt"
wildcardFile="/etc/dnsmasq.d/03-pihole-wildcard.conf" regexFile="${piholeDir}/regex.list"
adList="${piholeDir}/gravity.list" adList="${piholeDir}/gravity.list"
blackList="${piholeDir}/black.list" blackList="${piholeDir}/black.list"
@ -44,6 +44,10 @@ preEventHorizon="list.preEventHorizon"
skipDownload="false" skipDownload="false"
resolver="pihole-FTL"
haveSourceUrls=true
# Source setupVars from install script # Source setupVars from install script
setupVars="${piholeDir}/setupVars.conf" setupVars="${piholeDir}/setupVars.conf"
if [[ -f "${setupVars}" ]];then if [[ -f "${setupVars}" ]];then
@ -104,7 +108,7 @@ gravity_CheckDNSResolutionAvailable() {
fi fi
# Determine error output message # Determine error output message
if pidof dnsmasq &> /dev/null; then if pidof ${resolver} &> /dev/null; then
echo -e " ${CROSS} DNS resolution is currently unavailable" echo -e " ${CROSS} DNS resolution is currently unavailable"
else else
echo -e " ${CROSS} DNS service is not running" echo -e " ${CROSS} DNS service is not running"
@ -129,20 +133,12 @@ gravity_CheckDNSResolutionAvailable() {
gravity_GetBlocklistUrls() { gravity_GetBlocklistUrls() {
echo -e " ${INFO} ${COL_BOLD}Neutrino emissions detected${COL_NC}..." echo -e " ${INFO} ${COL_BOLD}Neutrino emissions detected${COL_NC}..."
# Determine if adlists file needs handling if [[ -f "${adListDefault}" ]] && [[ -f "${adListFile}" ]]; then
if [[ ! -f "${adListFile}" ]]; then
# Create "adlists.list" by copying "adlists.default" from internal core repo
cp "${adListRepoDefault}" "${adListFile}" 2> /dev/null || \
echo -e " ${CROSS} Unable to copy ${adListFile##*/} from ${piholeRepo}"
elif [[ -f "${adListDefault}" ]] && [[ -f "${adListFile}" ]]; then
# Remove superceded $adListDefault file # Remove superceded $adListDefault file
rm "${adListDefault}" 2> /dev/null || \ rm "${adListDefault}" 2> /dev/null || \
echo -e " ${CROSS} Unable to remove ${adListDefault}" echo -e " ${CROSS} Unable to remove ${adListDefault}"
fi fi
local str="Pulling blocklist source list into range"
echo -ne " ${INFO} ${str}..."
# Retrieve source URLs from $adListFile # Retrieve source URLs from $adListFile
# Logic: Remove comments and empty lines # Logic: Remove comments and empty lines
mapfile -t sources <<< "$(grep -v -E "^(#|$)" "${adListFile}" 2> /dev/null)" mapfile -t sources <<< "$(grep -v -E "^(#|$)" "${adListFile}" 2> /dev/null)"
@ -158,11 +154,15 @@ gravity_GetBlocklistUrls() {
}' <<< "$(printf '%s\n' "${sources[@]}")" 2> /dev/null }' <<< "$(printf '%s\n' "${sources[@]}")" 2> /dev/null
)" )"
local str="Pulling blocklist source list into range"
if [[ -n "${sources[*]}" ]] && [[ -n "${sourceDomains[*]}" ]]; then if [[ -n "${sources[*]}" ]] && [[ -n "${sourceDomains[*]}" ]]; then
echo -e "${OVER} ${TICK} ${str}" echo -e "${OVER} ${TICK} ${str}"
else else
echo -e "${OVER} ${CROSS} ${str}" echo -e "${OVER} ${CROSS} ${str}"
gravity_Cleanup "error" echo -e " ${INFO} No source list found, or it is empty"
echo ""
haveSourceUrls=false
fi fi
} }
@ -220,8 +220,15 @@ gravity_DownloadBlocklistFromUrl() {
httpCode=$(curl -s -L ${cmd_ext} ${heisenbergCompensator} -w "%{http_code}" -A "${agent}" "${url}" -o "${patternBuffer}" 2> /dev/null) httpCode=$(curl -s -L ${cmd_ext} ${heisenbergCompensator} -w "%{http_code}" -A "${agent}" "${url}" -o "${patternBuffer}" 2> /dev/null)
case $url in case $url in
# Did we "download" a local file?
"file"*)
if [[ -s "${patternBuffer}" ]]; then
echo -e "${OVER} ${TICK} ${str} Retrieval successful"; success=true
else
echo -e "${OVER} ${CROSS} ${str} Not found / empty list"
fi;;
# Did we "download" a remote file? # Did we "download" a remote file?
"http"*) *)
# Determine "Status:" output based on HTTP response # Determine "Status:" output based on HTTP response
case "${httpCode}" in case "${httpCode}" in
"200") echo -e "${OVER} ${TICK} ${str} Retrieval successful"; success=true;; "200") echo -e "${OVER} ${TICK} ${str} Retrieval successful"; success=true;;
@ -235,16 +242,8 @@ gravity_DownloadBlocklistFromUrl() {
"504") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Gateway)";; "504") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Gateway)";;
"521") echo -e "${OVER} ${CROSS} ${str} Web Server Is Down (Cloudflare)";; "521") echo -e "${OVER} ${CROSS} ${str} Web Server Is Down (Cloudflare)";;
"522") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Cloudflare)";; "522") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Cloudflare)";;
* ) echo -e "${OVER} ${CROSS} ${str} ${httpCode}";; * ) echo -e "${OVER} ${CROSS} ${str} ${url} (${httpCode})";;
esac;; esac;;
# Did we "download" a local file?
"file"*)
if [[ -s "${patternBuffer}" ]]; then
echo -e "${OVER} ${TICK} ${str} Retrieval successful"; success=true
else
echo -e "${OVER} ${CROSS} ${str} Not found / empty list"
fi;;
*) echo -e "${OVER} ${CROSS} ${str} ${url} ${httpCode}";;
esac esac
# Determine if the blocklist was downloaded and saved correctly # Determine if the blocklist was downloaded and saved correctly
@ -279,9 +278,9 @@ gravity_ParseFileIntoDomains() {
# Most of the lists downloaded are already in hosts file format but the spacing/formating is not contigious # Most of the lists downloaded are already in hosts file format but the spacing/formating is not contigious
# This helps with that and makes it easier to read # This helps with that and makes it easier to read
# It also helps with debugging so each stage of the script can be researched more in depth # It also helps with debugging so each stage of the script can be researched more in depth
#Awk -F splits on given IFS, we grab the right hand side (chops trailing #coments and /'s to grab the domain only. # Awk -F splits on given IFS, we grab the right hand side (chops trailing #coments and /'s to grab the domain only.
#Last awk command takes non-commented lines and if they have 2 fields, take the left field (the domain) and leave # Last awk command takes non-commented lines and if they have 2 fields, take the right field (the domain) and leave
#+ the right (IP address), otherwise grab the single field. # the left (IP address), otherwise grab the single field.
< ${source} awk -F '#' '{print $1}' | \ < ${source} awk -F '#' '{print $1}' | \
awk -F '/' '{print $1}' | \ awk -F '/' '{print $1}' | \
@ -345,13 +344,18 @@ gravity_ParseFileIntoDomains() {
# Scanning for "^IPv4$" is too slow with large (1M) lists on low-end hardware # Scanning for "^IPv4$" is too slow with large (1M) lists on low-end hardware
echo -ne " ${INFO} Format: URL" echo -ne " ${INFO} Format: URL"
awk '{ awk '
# Remove URL protocol, optional "username:password@", and ":?/;" # Remove URL scheme, optional "username:password@", and ":?/;"
if ($0 ~ /[:?\/;]/) { gsub(/(^.*:\/\/(.*:.*@)?|[:?\/;].*)/, "", $0) } # The scheme must be matched carefully to avoid blocking the wrong URL
# Remove lines which are only IPv4 addresses # in cases like:
if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { $0="" } # http://www.evil.com?http://www.good.com
if ($0) { print $0 } # See RFC 3986 section 3.1 for details.
}' "${source}" 2> /dev/null > "${destination}" /[:?\/;]/ { gsub(/(^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.*:.*@)?|[:?\/;].*)/, "", $0) }
# Skip lines which are only IPv4 addresses
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { next }
# Print if nonempty
length { print }
' "${source}" 2> /dev/null > "${destination}"
echo -e "${OVER} ${TICK} Format: URL" echo -e "${OVER} ${TICK} Format: URL"
else else
@ -371,7 +375,9 @@ gravity_ConsolidateDownloadedBlocklists() {
local str lastLine local str lastLine
str="Consolidating blocklists" str="Consolidating blocklists"
echo -ne " ${INFO} ${str}..." if [[ "${haveSourceUrls}" == true ]]; then
echo -ne " ${INFO} ${str}..."
fi
# Empty $matterAndLight if it already exists, otherwise, create it # Empty $matterAndLight if it already exists, otherwise, create it
: > "${piholeDir}/${matterAndLight}" : > "${piholeDir}/${matterAndLight}"
@ -390,8 +396,9 @@ gravity_ConsolidateDownloadedBlocklists() {
fi fi
fi fi
done done
if [[ "${haveSourceUrls}" == true ]]; then
echo -e "${OVER} ${TICK} ${str}" echo -e "${OVER} ${TICK} ${str}"
fi
} }
# Parse consolidated list into (filtered, unique) domains-only format # Parse consolidated list into (filtered, unique) domains-only format
@ -399,24 +406,33 @@ gravity_SortAndFilterConsolidatedList() {
local str num local str num
str="Extracting domains from blocklists" str="Extracting domains from blocklists"
echo -ne " ${INFO} ${str}..." if [[ "${haveSourceUrls}" == true ]]; then
echo -ne " ${INFO} ${str}..."
fi
# Parse into hosts file # Parse into hosts file
gravity_ParseFileIntoDomains "${piholeDir}/${matterAndLight}" "${piholeDir}/${parsedMatter}" gravity_ParseFileIntoDomains "${piholeDir}/${matterAndLight}" "${piholeDir}/${parsedMatter}"
# Format $parsedMatter line total as currency # Format $parsedMatter line total as currency
num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${parsedMatter}")") num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${parsedMatter}")")
echo -e "${OVER} ${TICK} ${str} if [[ "${haveSourceUrls}" == true ]]; then
${INFO} Number of domains being pulled in by gravity: ${COL_BLUE}${num}${COL_NC}" echo -e "${OVER} ${TICK} ${str}"
fi
echo -e " ${INFO} Number of domains being pulled in by gravity: ${COL_BLUE}${num}${COL_NC}"
str="Removing duplicate domains" str="Removing duplicate domains"
echo -ne " ${INFO} ${str}..." if [[ "${haveSourceUrls}" == true ]]; then
echo -ne " ${INFO} ${str}..."
fi
sort -u "${piholeDir}/${parsedMatter}" > "${piholeDir}/${preEventHorizon}" sort -u "${piholeDir}/${parsedMatter}" > "${piholeDir}/${preEventHorizon}"
echo -e "${OVER} ${TICK} ${str}"
# Format $preEventHorizon line total as currency if [[ "${haveSourceUrls}" == true ]]; then
num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${preEventHorizon}")") echo -e "${OVER} ${TICK} ${str}"
echo -e " ${INFO} Number of unique domains trapped in the Event Horizon: ${COL_BLUE}${num}${COL_NC}" # Format $preEventHorizon line total as currency
num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${preEventHorizon}")")
echo -e " ${INFO} Number of unique domains trapped in the Event Horizon: ${COL_BLUE}${num}${COL_NC}"
fi
} }
# Whitelist user-defined domains # Whitelist user-defined domains
@ -438,7 +454,7 @@ gravity_Whitelist() {
echo -e "${OVER} ${INFO} ${str}" echo -e "${OVER} ${INFO} ${str}"
} }
# Output count of blacklisted domains and wildcards # Output count of blacklisted domains and regex filters
gravity_ShowBlockCount() { gravity_ShowBlockCount() {
local num local num
@ -447,13 +463,9 @@ gravity_ShowBlockCount() {
echo -e " ${INFO} Number of blacklisted domains: ${num}" echo -e " ${INFO} Number of blacklisted domains: ${num}"
fi fi
if [[ -f "${wildcardFile}" ]]; then if [[ -f "${regexFile}" ]]; then
num=$(grep -c "^" "${wildcardFile}") num=$(grep -c "^(?!#)" "${regexFile}")
# If IPv4 and IPv6 is used, divide total wildcard count by 2 echo -e " ${INFO} Number of regex filters: ${num}"
if [[ -n "${IPV4_ADDRESS}" ]] && [[ -n "${IPV6_ADDRESS}" ]];then
num=$(( num/2 ))
fi
echo -e " ${INFO} Number of wildcard blocked domains: ${num}"
fi fi
} }
@ -505,12 +517,12 @@ gravity_ParseBlacklistDomains() {
# Empty $accretionDisc if it already exists, otherwise, create it # Empty $accretionDisc if it already exists, otherwise, create it
: > "${piholeDir}/${accretionDisc}" : > "${piholeDir}/${accretionDisc}"
if [[ -f "${piholeDir}/${whitelistMatter}" ]]; then if [[ -f "${piholeDir}/${whitelistMatter}" ]]; then
gravity_ParseDomainsIntoHosts "${piholeDir}/${whitelistMatter}" "${piholeDir}/${accretionDisc}" mv "${piholeDir}/${whitelistMatter}" "${piholeDir}/${accretionDisc}"
else else
# There was no whitelist file, so use preEventHorizon instead of whitelistMatter. # There was no whitelist file, so use preEventHorizon instead of whitelistMatter.
gravity_ParseDomainsIntoHosts "${piholeDir}/${preEventHorizon}" "${piholeDir}/${accretionDisc}" mv "${piholeDir}/${preEventHorizon}" "${piholeDir}/${accretionDisc}"
fi fi
# Move the file over as /etc/pihole/gravity.list so dnsmasq can use it # Move the file over as /etc/pihole/gravity.list so dnsmasq can use it
@ -528,11 +540,9 @@ gravity_ParseUserDomains() {
if [[ ! -f "${blacklistFile}" ]]; then if [[ ! -f "${blacklistFile}" ]]; then
return 0 return 0
fi fi
gravity_ParseDomainsIntoHosts "${blacklistFile}" "${blackList}.tmp"
# Copy the file over as /etc/pihole/black.list so dnsmasq can use it # Copy the file over as /etc/pihole/black.list so dnsmasq can use it
mv "${blackList}.tmp" "${blackList}" 2> /dev/null || \ cp "${blacklistFile}" "${blackList}" 2> /dev/null || \
echo -e "\\n ${CROSS} Unable to move ${blackList##*/}.tmp to ${piholeDir}" echo -e "\\n ${CROSS} Unable to move ${blacklistFile##*/} to ${piholeDir}"
} }
# Trap Ctrl-C # Trap Ctrl-C
@ -567,7 +577,7 @@ gravity_Cleanup() {
echo -e "${OVER} ${TICK} ${str}" echo -e "${OVER} ${TICK} ${str}"
# Only restart DNS service if offline # Only restart DNS service if offline
if ! pidof dnsmasq &> /dev/null; then if ! pidof ${resolver} &> /dev/null; then
"${PIHOLE_COMMAND}" restartdns "${PIHOLE_COMMAND}" restartdns
dnsWasOffline=true dnsWasOffline=true
fi fi
@ -616,7 +626,9 @@ if [[ "${skipDownload}" == false ]]; then
# Gravity needs to download blocklists # Gravity needs to download blocklists
gravity_CheckDNSResolutionAvailable gravity_CheckDNSResolutionAvailable
gravity_GetBlocklistUrls gravity_GetBlocklistUrls
gravity_SetDownloadOptions if [[ "${haveSourceUrls}" == true ]]; then
gravity_SetDownloadOptions
fi
gravity_ConsolidateDownloadedBlocklists gravity_ConsolidateDownloadedBlocklists
gravity_SortAndFilterConsolidatedList gravity_SortAndFilterConsolidatedList
else else
@ -631,6 +643,7 @@ if [[ "${skipDownload}" == false ]] || [[ "${listType}" == "whitelist" ]]; then
gravity_Whitelist gravity_Whitelist
fi fi
convert_wildcard_to_regex
gravity_ShowBlockCount gravity_ShowBlockCount
# Perform when downloading blocklists, or modifying the white/blacklist (not wildcards) # Perform when downloading blocklists, or modifying the white/blacklist (not wildcards)

@ -0,0 +1,112 @@
.TH "Pihole-FTL" "8" "pihole-FTL" "Pi-hole" "June 2018"
.SH "NAME"
pihole-FTL - Pi-hole : The Faster-Than-Light (FTL) Engine
.br
.SH "SYNOPSIS"
\fBservice pihole-FTL \fR(\fBstart\fR|\fBstop\fR|\fBrestart\fR)
.br
\fBpihole-FTL debug\fR
.br
\fBpihole-FTL test\fR
.br
\fBpihole-FTL -v\fR
.br
\fBpihole-FTL -t\fR
.br
\fBpihole-FTL -b\fR
.br
\fBpihole-FTL -f\fR
.br
\fBpihole-FTL -h\fR
.br
\fBpihole-FTL dnsmasq-test\fR
.br
\fBpihole-FTL --\fR (\fBoptions\fR)
.br
.SH "DESCRIPTION"
Pi-hole : The Faster-Than-Light (FTL) Engine is a lightweight, purpose-built daemon used to provide statistics needed for the Pi-hole Web Interface, and its API can be easily integrated into your own projects. Although it is an optional component of the Pi-hole ecosystem, it will be installed by default to provide statistics. As the name implies, FTL does its work \fIvery\fR \fIquickly\fR!
.br
Usage
.br
\fBservice pihole-FTL start\fR
.br
Start the pihole-FTL daemon
.br
\fBservice pihole-FTL stop\fR
.br
Stop the pihole-FTL daemon
.br
\fBservice pihole-FTL restart\fR
.br
If the pihole-FTP daemon is running, stop and then start, otherwise start.
.br
Command line arguments
.br
\fBdebug\fR
.br
Don't go into daemon mode (stay in foreground) + more verbose logging
.br
\fBtest\fR
.br
Start FTL and process everything, but shut down immediately afterwards
.br
\fB-v, version\fR
.br
Don't start FTL, show only version
.br
\fB-t, tag\fR
.br
Don't start FTL, show only git tag
.br
\fB-b, branch\fR
.br
Don't start FTL, show only git branch FTL was compiled from
.br
\fB-f, no-daemon\fR
.br
Don't go into background (daemon mode)
.br
\fB-h, help\fR
.br
Don't start FTL, show help
.br
\fBdnsmasq-test\fR
.br
Test resolver config file syntax
.br
\fB--\fR (options)
.br
Pass options to internal dnsmasq resolver
.br
.SH "EXAMPLE"
Command line arguments can be arbitrarily combined, e.g:
.br
\fBpihole-FTL debug test\fR
.br
Start ftl in foreground with more verbose logging, process everything and shutdown immediately
.br
.SH "SEE ALSO"
\fBpihole\fR(8), \fBpihole-FTL.conf\fR(5)
.br
.SH "COLOPHON"
Get sucked into the latest news and community activity by entering Pi-hole's orbit. Information about Pi-hole, and the latest version of the software can be found at https://pi-hole.net
.br

@ -0,0 +1,102 @@
.TH "pihole-FTL.conf" "5" "pihole-FTL.conf" "pihole-FTL.conf" "June 2018"
.SH "NAME"
pihole-FTL.conf - FTL's config file
.br
.SH "DESCRIPTION"
/etc/pihole/pihole-FTL.conf will be read by \fBpihole-FTL(8)\fR on startup.
.br
\fBSOCKET_LISTENING=localonly|all\fR
.br
Listen only for local socket connections or permit all connections
.br
\fBQUERY_DISPLAY=yes|no\fR
.br
Display all queries? Set to no to hide query display
.br
\fBAAAA_QUERY_ANALYSIS=yes|no\fR
.br
Allow FTL to analyze AAAA queries from pihole.log?
.br
\fBRESOLVE_IPV6=yes|no\fR
.br
Should FTL try to resolve IPv6 addresses to host names?
.br
\fBRESOLVE_IPV4=yes|no\fR
.br
Should FTL try to resolve IPv4 addresses to host names?
.br
\fBMAXDBDAYS=365\fR
.br
How long should queries be stored in the database?
.br
Setting this to 0 disables the database
.br
\fBDBINTERVAL=1.0\fR
.br
How often do we store queries in FTL's database [minutes]?
.br
\fBDBFILE=/etc/pihole/pihole-FTL.db\fR
.br
Specify path and filename of FTL's SQLite long-term database.
.br
Setting this to DBFILE= disables the database altogether
.br
\fBMAXLOGAGE=24.0\fR
.br
Up to how many hours of queries should be imported from the database and logs?
.br
Maximum is 744 (31 days)
.br
\fBFTLPORT=4711\fR
.br
On which port should FTL be listening?
.br
\fBPRIVACYLEVEL=0|1|2|3\fR
.br
Which privacy level is used?
.br
0 - show everything
.br
1 - hide domains
.br
2 - hide domains and clients
.br
3 - paranoia mode (hide everything)
.br
\fBIGNORE_LOCALHOST=no|yes\fR
.br
Should FTL ignore queries coming from the local machine?
.br
\fBBLOCKINGMODE=IP|IP-AAAA-NODATA|NXDOMAIN|NULL\fR
.br
How should FTL reply to blocked queries?
.br
For each setting, the option shown first is the default.
.br
.SH "SEE ALSO"
\fBpihole\fR(8), \fBpihole-FTL\fR(8)
.br
.SH "COLOPHON"
Pi-hole : The Faster-Than-Light (FTL) Engine is a lightweight, purpose-built daemon used to provide statistics needed for the Pi-hole Web Interface, and its API can be easily integrated into your own projects. Although it is an optional component of the Pi-hole ecosystem, it will be installed by default to provide statistics. As the name implies, FTL does its work \fIvery quickly\fR!
.br
Get sucked into the latest news and community activity by entering Pi-hole's orbit. Information about Pi-hole, and the latest version of the software can be found at https://pi-hole.net
.br

@ -0,0 +1,361 @@
.TH "Pi-hole" "8" "Pi-hole" "Pi-hole" "May 2018"
.SH "NAME"
Pi-hole : A black-hole for internet advertisements
.br
.SH "SYNOPSIS"
\fBpihole\fR (\fB-w\fR|\fB-b\fR|\fB--wild\fR|\fB--regex\fR) [options] domain(s)
.br
\fBpihole -a\fR \fB-p\fR password
.br
\fBpihole -a\fR (\fB-c|-f|-k\fR)
.br
\fBpihole -a\fR [\fB-r\fR hostrecord]
.br
\fBpihole -a -e\fR email
.br
\fBpihole -a -i\fR interface
.br
\fBpihole -a -l\fR privacylevel
.br
\fBpihole -c\fR [-j|-r|-e]
.br
\fBpihole\fR \fB-d\fR [-a]
.br
\fBpihole -f
.br
pihole -r
.br
pihole -t
.br
pihole -g\fR
.br
\fBpihole\fR -\fBq\fR [options]
.br
\fBpihole\fR \fB-l\fR (\fBon|off|off noflush\fR)
.br
\fBpihole -up \fR[--checkonly]
.br
\fBpihole -v\fR [-p|-a|-f] [-c|-l|-hash]
.br
\fBpihole uninstall
.br
pihole status
.br
pihole restartdns\fR
.br
\fBpihole\fR (\fBenable\fR|\fBdisable\fR [time])
.br
\fBpihole\fR \fBcheckout\fR repo [branch]
.br
\fBpihole\fR \fBhelp\fR
.br
.SH "DESCRIPTION"
Available commands and options:
.br
\fB-w, whitelist\fR [options] [<domain1> <domain2 ...>]
.br
Adds or removes specified domain or domains tho the Whitelist
.br
\fB-b, blacklist\fR [options] [<domain1> <domain2 ...>]
.br
Adds or removes specified domain or domains to the blacklist
.br
\fB--wild, wildcard\fR [options] [<domain1> <domain2 ...>]
.br
Add or removes specified domain to the wildcard blacklist
.br
\fB--regex, regex\fR [options] [<regex1> <regex2 ...>]
.br
Add or removes specified regex filter to the regex blacklist
.br
(Whitelist/Blacklist manipulation options):
.br
-d, --delmode Remove domain(s) from the list
.br
-nr, --noreload Update list without refreshing dnsmasq
.br
-q, --quiet Make output less verbose
.br
-l, --list Display all your listed domains
.br
--nuke Removes all entries in a list
.br
\fB-d, debug\fR [-a]
.br
Start a debugging session
.br
-a Enable automated debugging
.br
\fB-f, flush\fR
.br
Flush the Pi-hole log
.br
\fB-r, reconfigure\fR
.br
Reconfigure or Repair Pi-hole subsystems
.br
\fB-t, tail\fR
.br
View the live output of the Pi-hole log
.br
\fB-a, admin\fR [options]
.br
(Admin options):
.br
-p, password Set Web Interface password
.br
-c, celsius Set Celsius as preferred temperature unit
.br
-f, fahrenheit Set Fahrenheit as preferred temperature unit
.br
-k, kelvin Set Kelvin as preferred temperature unit
.br
-r, hostrecord Add a name to the DNS associated to an
IPv4/IPv6 address
.br
-e, email Set an administrative contact address for the
Block Page
.br
-i, interface Specify dnsmasq's interface listening behavior
.br
-l, privacylevel <level> Set privacy level
(0 = lowest, 3 = highest)
.br
\fB-c, chronometer\fR [options]
.br
Calculates stats and displays to an LCD
.br
(Chronometer Options):
.br
-j, --json Output stats as JSON formatted string
.br
-r, --refresh Set update frequency (in seconds)
.br
-e, --exit Output stats and exit witout refreshing
.br
\fB-g, updateGravity\fR
.br
Update the list of ad-serving domains
.br
\fB-q, query\fR [option]
.br
Query the adlists for a specified domain
.br
(Query options):
.br
-adlist Print the name of the block list URL
.br
-exact Search the block lists for exact domain matches
.br
-all Return all query matches within a block list
.br
\fB-h, --help, help\fR
.br
Show a help dialog
.br
\fB-l, logging\fR [on|off|off noflush]
.br
Specify whether the Pi-hole log should be used
.br
(Logging options):
.br
on Enable the Pi-hole log at /var/log/pihole.log
.br
off Disable and flush the Pi-hole log at
/var/log/pihole.log
.br
off noflush Disable the Pi-hole log at /var/log/pihole.log
.br
\fB-up, updatePihole\fR [--check-only]
.br
Update Pi-hole subsystems
.br
--check-only Exit script before update is performed.
.br
\fB-v, version\fR [repo] [options]
.br
Show installed versions of Pi-hole, Web Interface &amp; FTL
.br
.br
(repo options):
.br
-p, --pihole Only retrieve info regarding Pi-hole repository
.br
-a, --admin Only retrieve info regarding AdminLTE
repository
.br
-f, --ftl Only retrieve info regarding FTL repository
.br
(version options):
.br
-c, --current Return the current version
.br
-l, --latest Return the latest version
.br
--hash Return the Github hash from your local
repositories
.br
\fBuninstall\fR
.br
Uninstall Pi-hole from your system
.br
\fBstatus\fR
.br
Display the running status of Pi-hole subsystems
.br
\fBenable\fR
.br
Enable Pi-hole subsystems
.br
\fBdisable\fR [time]
.br
Disable Pi-hole subsystems, optionally for a set duration
.br
(time options):
.br
#s Disable Pi-hole functionality for # second(s)
.br
#m Disable Pi-hole functionality for # minute(s)
.br
\fBrestartdns\fR
.br
Restart Pi-hole subsystems
.br
\fBcheckout\fR [repo] [branch]
.br
Switch Pi-hole subsystems to a different Github branch
.br
(repo options):
.br
core Change the branch of Pi-hole's core subsystem
.br
web Change the branch of Admin Console subsystem
.br
ftl Change the branch of Pi-hole's FTL subsystem
.br
(branch options):
.br
master Update subsystems to the latest stable release
.br
dev Update subsystems to the latest development
release
.br
branchname Update subsystems to the specified branchname
.br
.SH "EXAMPLE"
Some usage examples
.br
Whitelist/blacklist manipulation
.br
\fBpihole -w iloveads.example.com\fR
.br
Adds "iloveads.example.com" to whitelist
.br
\fBpihole -b -d noads.example.com\fR
.br
Removes "noads.example.com" from blacklist
.br
\fBpihole --wild example.com\fR
.br
Adds example.com as a wildcard - would block all subdomains of
example.com, including example.com itself.
.br
\fBpihole --regex "ad.*\\.example\\.com$"\fR
.br
Adds "ad.*\\.example\\.com$" to the regex blacklist.
Would block all subdomains of example.com which start with "ad"
.br
Changing the Web Interface password
.br
\fBpihole -a -p ExamplePassword\fR
.br
Change the password to "ExamplePassword"
.br
Updating lists from internet sources
.br
\fBpihole -g\fR
.br
Update the list of ad-serving domains
.br
Displaying version information
.br
\fBpihole -v -a -c\fR
.br
Display the current version of AdminLTE
.br
Temporarily disabling Pi-hole
.br
\fBpihole disable 5m\fR
.br
Disable Pi-hole functionality for five minutes
.br
Switching Pi-hole subsystem branches
.br
\fBpihole checkout master\fR
.br
Switch to master branch
.br
\fBpihole checkout core dev\fR
.br
Switch to core development branch
.br
.SH "SEE ALSO"
\fBlighttpd\fR(8), \fBpihole-FTL\fR(8)
.br
.SH "COLOPHON"
Get sucked into the latest news and community activity by entering Pi-hole's orbit. Information about Pi-hole, and the latest version of the software can be found at https://pi-hole.net.
.br

286
pihole

@ -14,6 +14,8 @@ readonly wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf"
readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
source "${colfile}" source "${colfile}"
resolver="pihole-FTL"
# Must be root to use this tool # Must be root to use this tool
if [[ ! $EUID -eq 0 ]];then if [[ ! $EUID -eq 0 ]];then
if [[ -x "$(command -v sudo)" ]]; then if [[ -x "$(command -v sudo)" ]]; then
@ -31,17 +33,7 @@ webpageFunc() {
exit 0 exit 0
} }
whitelistFunc() { listFunc() {
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
exit 0
}
blacklistFunc() {
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
exit 0
}
wildcardFunc() {
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@" "${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
exit 0 exit 0
} }
@ -69,7 +61,8 @@ flushFunc() {
} }
updatePiholeFunc() { updatePiholeFunc() {
"${PI_HOLE_SCRIPT_DIR}"/update.sh shift
"${PI_HOLE_SCRIPT_DIR}"/update.sh "$@"
exit 0 exit 0
} }
@ -83,230 +76,9 @@ updateGravityFunc() {
exit 0 exit 0
} }
# Scan an array of files for matching strings
scanList(){
# Escape full stops
local domain="${1//./\\.}" lists="${2}" type="${3:-}"
# Prevent grep from printing file path
cd "/etc/pihole" || exit 1
# Prevent grep -i matching slowly: http://bit.ly/2xFXtUX
export LC_CTYPE=C
# /dev/null forces filename to be printed when only one list has been generated
# shellcheck disable=SC2086
case "${type}" in
"exact" ) grep -i -E -l "(^|\\s)${domain}($|\\s|#)" ${lists} /dev/null;;
"wc" ) grep -i -o -m 1 "/${domain}/" ${lists};;
* ) grep -i "${domain}" ${lists} /dev/null;;
esac
}
# Print each subdomain
# e.g: foo.bar.baz.com = "foo.bar.baz.com bar.baz.com baz.com com"
processWildcards() {
IFS="." read -r -a array <<< "${1}"
for (( i=${#array[@]}-1; i>=0; i-- )); do
ar=""
for (( j=${#array[@]}-1; j>${#array[@]}-i-2; j-- )); do
if [[ $j == $((${#array[@]}-1)) ]]; then
ar="${array[$j]}"
else
ar="${array[$j]}.${ar}"
fi
done
echo "${ar}"
done
}
queryFunc() { queryFunc() {
shift shift
local options="$*" adlist="" all="" exact="" blockpage="" matchType="match" "${PI_HOLE_SCRIPT_DIR}"/query.sh "$@"
if [[ "${options}" == "-h" ]] || [[ "${options}" == "--help" ]]; then
echo "Usage: pihole -q [option] <domain>
Example: 'pihole -q -exact domain.com'
Query the adlists for a specified domain
Options:
-adlist Print the name of the block list URL
-exact Search the block lists for exact domain matches
-all Return all query matches within a block list
-h, --help Show this help dialog"
exit 0
fi
if [[ ! -e "/etc/pihole/adlists.list" ]]; then
echo -e "${COL_LIGHT_RED}The file '/etc/pihole/adlists.list' was not found${COL_NC}"
exit 1
fi
# Handle valid options
if [[ "${options}" == *"-bp"* ]]; then
exact="exact"; blockpage=true
else
[[ "${options}" == *"-adlist"* ]] && adlist=true
[[ "${options}" == *"-all"* ]] && all=true
if [[ "${options}" == *"-exact"* ]]; then
exact="exact"; matchType="exact ${matchType}"
fi
fi
# Strip valid options, leaving only the domain and invalid options
# This allows users to place the options before or after the domain
options=$(sed -E 's/ ?-(bp|adlists?|all|exact) ?//g' <<< "${options}")
# Handle remaining options
# If $options contain non ASCII characters, convert to punycode
case "${options}" in
"" ) str="No domain specified";;
*" "* ) str="Unknown query option specified";;
*[![:ascii:]]* ) domainQuery=$(idn2 "${options}");;
* ) domainQuery="${options}";;
esac
if [[ -n "${str:-}" ]]; then
echo -e "${str}${COL_NC}\\nTry 'pihole -q --help' for more information."
exit 1
fi
# Scan Whitelist and Blacklist
lists="whitelist.txt blacklist.txt"
mapfile -t results <<< "$(scanList "${domainQuery}" "${lists}" "${exact}")"
if [[ -n "${results[*]}" ]]; then
wbMatch=true
# Loop through each result in order to print unique file title once
for result in "${results[@]}"; do
fileName="${result%%.*}"
if [[ -n "${blockpage}" ]]; then
echo "π ${result}"
exit 0
elif [[ -n "${exact}" ]]; then
echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}"
else
# Only print filename title once per file
if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then
echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}"
fileName_prev="${fileName}"
fi
echo " ${result#*:}"
fi
done
fi
# Scan Wildcards
if [[ -e "${wildcardlist}" ]]; then
# Determine all subdomains, domain and TLDs
mapfile -t wildcards <<< "$(processWildcards "${domainQuery}")"
for match in "${wildcards[@]}"; do
# Search wildcard list for matches
mapfile -t results <<< "$(scanList "${match}" "${wildcardlist}" "wc")"
if [[ -n "${results[*]}" ]]; then
if [[ -z "${wcMatch:-}" ]] && [[ -z "${blockpage}" ]]; then
wcMatch=true
echo " ${matchType^} found in ${COL_BOLD}Wildcards${COL_NC}:"
fi
case "${blockpage}" in
true ) echo "π ${wildcardlist##*/}"; exit 0;;
* ) echo " *.${match}";;
esac
fi
done
fi
# Get version sorted *.domains filenames (without dir path)
lists=("$(cd "/etc/pihole" || exit 0; printf "%s\\n" -- *.domains | sort -V)")
# Query blocklists for occurences of domain
mapfile -t results <<< "$(scanList "${domainQuery}" "${lists[*]}" "${exact}")"
# Handle notices
if [[ -z "${wbMatch:-}" ]] && [[ -z "${wcMatch:-}" ]] && [[ -z "${results[*]}" ]]; then
echo -e " ${INFO} No ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC} found within block lists"
exit 0
elif [[ -z "${results[*]}" ]]; then
# Result found in WL/BL/Wildcards
exit 0
elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 100 ]]; then
echo -e " ${INFO} Over 100 ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC}
This can be overridden using the -all option"
exit 0
fi
# Remove unwanted content from non-exact $results
if [[ -z "${exact}" ]]; then
# Delete lines starting with #
# Remove comments after domain
# Remove hosts format IP address
mapfile -t results <<< "$(IFS=$'\n'; sed \
-e "/:#/d" \
-e "s/[ \\t]#.*//g" \
-e "s/:.*[ \\t]/:/g" \
<<< "${results[*]}")"
# Exit if result was in a comment
[[ -z "${results[*]}" ]] && exit 0
fi
# Get adlist file content as array
if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then
for adlistUrl in $(< "/etc/pihole/adlists.list"); do
if [[ "${adlistUrl:0:4}" =~ (http|www.) ]]; then
adlists+=("${adlistUrl}")
fi
done
fi
# Print "Exact matches for" title
if [[ -n "${exact}" ]] && [[ -z "${blockpage}" ]]; then
plural=""; [[ "${#results[*]}" -gt 1 ]] && plural="es"
echo " ${matchType^}${plural} for ${COL_BOLD}${domainQuery}${COL_NC} found in:"
fi
for result in "${results[@]}"; do
fileName="${result/:*/}"
# Determine *.domains URL using filename's number
if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then
fileNum="${fileName/list./}"; fileNum="${fileNum%%.*}"
fileName="${adlists[$fileNum]}"
# Discrepency occurs when adlists has been modified, but Gravity has not been run
if [[ -z "${fileName}" ]]; then
fileName="${COL_LIGHT_RED}(no associated adlists URL found)${COL_NC}"
fi
fi
if [[ -n "${blockpage}" ]]; then
echo "${fileNum} ${fileName}"
elif [[ -n "${exact}" ]]; then
echo " ${fileName}"
else
if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then
count=""
echo " ${matchType^} found in ${COL_BOLD}${fileName}${COL_NC}:"
fileName_prev="${fileName}"
fi
: $((count++))
# Print matching domain if $max_count has not been reached
[[ -z "${all}" ]] && max_count="50"
if [[ -z "${all}" ]] && [[ "${count}" -ge "${max_count}" ]]; then
[[ "${count}" -gt "${max_count}" ]] && continue
echo " ${COL_GRAY}Over ${count} results found, skipping rest of file${COL_NC}"
else
echo " ${result#*:}"
fi
fi
done
exit 0 exit 0
} }
@ -332,18 +104,18 @@ restartDNS() {
local svcOption svc str output status local svcOption svc str output status
svcOption="${1:-}" svcOption="${1:-}"
# Determine if we should reload or restart dnsmasq # Determine if we should reload or restart restart
if [[ "${svcOption}" =~ "reload" ]]; then if [[ "${svcOption}" =~ "reload" ]]; then
# Using SIGHUP will NOT re-read any *.conf files # Using SIGHUP will NOT re-read any *.conf files
svc="killall -s SIGHUP dnsmasq" svc="killall -s SIGHUP ${resolver}"
else else
# Get PID of dnsmasq to determine if it needs to start or restart # Get PID of resolver to determine if it needs to start or restart
if pidof dnsmasq &> /dev/null; then if pidof pihole-FTL &> /dev/null; then
svcOption="restart" svcOption="restart"
else else
svcOption="start" svcOption="start"
fi fi
svc="service dnsmasq ${svcOption}" svc="service ${resolver} ${svcOption}"
fi fi
# Print output to Terminal, but not to Web Admin # Print output to Terminal, but not to Web Admin
@ -359,9 +131,6 @@ restartDNS() {
[[ ! -t 1 ]] && local OVER="" [[ ! -t 1 ]] && local OVER=""
echo -e "${OVER} ${CROSS} ${output}" echo -e "${OVER} ${CROSS} ${output}"
fi fi
# Send signal to FTL to have it re-parse the gravity files
killall -s SIGHUP pihole-FTL
} }
piholeEnable() { piholeEnable() {
@ -476,7 +245,7 @@ statusFunc() {
local addnConfigs local addnConfigs
# Determine if service is running on port 53 (Cr: https://superuser.com/a/806331) # Determine if service is running on port 53 (Cr: https://superuser.com/a/806331)
if (echo > /dev/tcp/localhost/53) >/dev/null 2>&1; then if (echo > /dev/tcp/127.0.0.1/53) >/dev/null 2>&1; then
if [[ "${1}" != "web" ]]; then if [[ "${1}" != "web" ]]; then
echo -e " ${TICK} DNS service is running" echo -e " ${TICK} DNS service is running"
fi fi
@ -516,6 +285,13 @@ statusFunc() {
} }
tailFunc() { tailFunc() {
# Warn user if Pi-hole's logging is disabled
local logging_enabled=$(grep -c "^log-queries" /etc/dnsmasq.d/01-pihole.conf)
if [[ "${logging_enabled}" == "0" ]]; then
# No "log-queries" lines are found.
# Commented out lines (such as "#log-queries") are ignored
echo " ${CROSS} Warning: Query logging is disabled"
fi
echo -e " ${INFO} Press Ctrl-C to exit" echo -e " ${INFO} Press Ctrl-C to exit"
# Retrieve IPv4/6 addresses # Retrieve IPv4/6 addresses
@ -541,12 +317,13 @@ Switch Pi-hole subsystems to a different Github branch
Repositories: Repositories:
core [branch] Change the branch of Pi-hole's core subsystem core [branch] Change the branch of Pi-hole's core subsystem
web [branch] Change the branch of Admin Console subsystem web [branch] Change the branch of Web Interface subsystem
ftl [branch] Change the branch of Pi-hole's FTL subsystem ftl [branch] Change the branch of Pi-hole's FTL subsystem
Branches: Branches:
master Update subsystems to the latest stable release master Update subsystems to the latest stable release
dev Update subsystems to the latest development release" dev Update subsystems to the latest development release
branchname Update subsystems to the specified branchname"
exit 0 exit 0
fi fi
@ -599,7 +376,8 @@ Add '-h' after specific commands for more information on usage
Whitelist/Blacklist Options: Whitelist/Blacklist Options:
-w, whitelist Whitelist domain(s) -w, whitelist Whitelist domain(s)
-b, blacklist Blacklist domain(s) -b, blacklist Blacklist domain(s)
-wild, wildcard Blacklist domain(s), and all its subdomains --wild, wildcard Wildcard blacklist domain(s)
--regex, regex Regex blacklist domains(s)
Add '-h' for more info on whitelist/blacklist usage Add '-h' for more info on whitelist/blacklist usage
Debugging Options: Debugging Options:
@ -610,8 +388,8 @@ Debugging Options:
-t, tail View the live output of the Pi-hole log -t, tail View the live output of the Pi-hole log
Options: Options:
-a, admin Admin Console options -a, admin Web interface options
Add '-h' for more info on admin console usage Add '-h' for more info on Web Interface usage
-c, chronometer Calculates stats and displays to an LCD -c, chronometer Calculates stats and displays to an LCD
Add '-h' for more info on chronometer usage Add '-h' for more info on chronometer usage
-g, updateGravity Update the list of ad-serving domains -g, updateGravity Update the list of ad-serving domains
@ -621,7 +399,8 @@ Options:
-q, query Query the adlists for a specified domain -q, query Query the adlists for a specified domain
Add '-h' for more info on query usage Add '-h' for more info on query usage
-up, updatePihole Update Pi-hole subsystems -up, updatePihole Update Pi-hole subsystems
-v, version Show installed versions of Pi-hole, Admin Console & FTL Add '--check-only' to exit script before update is performed.
-v, version Show installed versions of Pi-hole, Web Interface & FTL
Add '-h' for more info on version usage Add '-h' for more info on version usage
uninstall Uninstall Pi-hole from your system uninstall Uninstall Pi-hole from your system
status Display the running status of Pi-hole subsystems status Display the running status of Pi-hole subsystems
@ -640,12 +419,13 @@ fi
# Handle redirecting to specific functions based on arguments # Handle redirecting to specific functions based on arguments
case "${1}" in case "${1}" in
"-w" | "whitelist" ) whitelistFunc "$@";; "-w" | "whitelist" ) listFunc "$@";;
"-b" | "blacklist" ) blacklistFunc "$@";; "-b" | "blacklist" ) listFunc "$@";;
"-wild" | "wildcard" ) wildcardFunc "$@";; "--wild" | "wildcard" ) listFunc "$@";;
"--regex" | "regex" ) listFunc "$@";;
"-d" | "debug" ) debugFunc "$@";; "-d" | "debug" ) debugFunc "$@";;
"-f" | "flush" ) flushFunc "$@";; "-f" | "flush" ) flushFunc "$@";;
"-up" | "updatePihole" ) updatePiholeFunc;; "-up" | "updatePihole" ) updatePiholeFunc "$@";;
"-r" | "reconfigure" ) reconfigurePiholeFunc;; "-r" | "reconfigure" ) reconfigurePiholeFunc;;
"-g" | "updateGravity" ) updateGravityFunc "$@";; "-g" | "updateGravity" ) updateGravityFunc "$@";;
"-c" | "chronometer" ) chronometerFunc "$@";; "-c" | "chronometer" ) chronometerFunc "$@";;

@ -3,3 +3,4 @@ pytest
pytest-xdist pytest-xdist
pytest-cov pytest-cov
testinfra testinfra
tox

@ -0,0 +1,6 @@
from setuptools import setup
setup(
setup_requires=['pytest-runner'],
tests_require=['pytest'],
)

@ -0,0 +1,25 @@
# Recommended way to run tests
Make sure you have Docker and Python w/pip package manager.
From command line all you need to do is:
- `pip install tox`
- `tox`
Tox handles setting up a virtual environment for python dependancies, installing dependancies, building the docker images used by tests, and finally running tests. It's an easy way to have travis-ci like build behavior locally.
## Alternative py.test method of running tests
You're responsible for setting up your virtual env and dependancies in this situation.
```
py.test -vv -n auto -m "build_stage"
py.test -vv -n auto -m "not build_stage"
```
The build_stage tests have to run first to create the docker images, followed by the actual tests which utilize said images. Unless you're changing your dockerfiles you shouldn't have to run the build_stage every time - but it's a good idea to rebuild at least once a day in case the base Docker images or packages change.
# How do I debug python?
Highly recommended: Setup PyCharm on a **Docker enabled** machine. Having a python debugger like PyCharm changes your life if you've never used it :)

@ -1,14 +1,30 @@
import pytest import pytest
import testinfra import testinfra
from textwrap import dedent
check_output = testinfra.get_backend( check_output = testinfra.get_backend(
"local://" "local://"
).get_module("Command").check_output ).get_module("Command").check_output
SETUPVARS = {
'PIHOLE_INTERFACE': 'eth99',
'IPV4_ADDRESS': '1.1.1.1',
'IPV6_ADDRESS': 'FE80::240:D0FF:FE48:4672',
'PIHOLE_DNS_1': '4.2.2.1',
'PIHOLE_DNS_2': '4.2.2.2'
}
tick_box = "[\x1b[1;32m\xe2\x9c\x93\x1b[0m]".decode("utf-8")
cross_box = "[\x1b[1;31m\xe2\x9c\x97\x1b[0m]".decode("utf-8")
info_box = "[i]".decode("utf-8")
@pytest.fixture @pytest.fixture
def Pihole(Docker): def Pihole(Docker):
''' used to contain some script stubbing, now pretty much an alias. '''
Also provides bash as the default run function shell ''' used to contain some script stubbing, now pretty much an alias.
Also provides bash as the default run function shell
'''
def run_bash(self, command, *args, **kwargs): def run_bash(self, command, *args, **kwargs):
cmd = self.get_command(command, *args) cmd = self.get_command(command, *args)
if self.user is not None: if self.user is not None:
@ -22,12 +38,18 @@ def Pihole(Docker):
return out return out
funcType = type(Docker.run) funcType = type(Docker.run)
Docker.run = funcType(run_bash, Docker, testinfra.backend.docker.DockerBackend) Docker.run = funcType(run_bash,
Docker,
testinfra.backend.docker.DockerBackend)
return Docker return Docker
@pytest.fixture @pytest.fixture
def Docker(request, args, image, cmd): def Docker(request, args, image, cmd):
''' combine our fixtures into a docker run command and setup finalizer to cleanup ''' '''
combine our fixtures into a docker run command and setup finalizer to
cleanup
'''
assert 'docker' in check_output('id'), "Are you in the docker group?" assert 'docker' in check_output('id'), "Are you in the docker group?"
docker_run = "docker run {} {} {}".format(args, image, cmd) docker_run = "docker run {} {} {}".format(args, image, cmd)
docker_id = check_output(docker_run) docker_id = check_output(docker_run)
@ -40,22 +62,95 @@ def Docker(request, args, image, cmd):
docker_container.id = docker_id docker_container.id = docker_id
return docker_container return docker_container
@pytest.fixture @pytest.fixture
def args(request): def args(request):
''' -t became required when tput began being used ''' '''
-t became required when tput began being used
'''
return '-t -d' return '-t -d'
@pytest.fixture(params=['debian', 'centos'])
@pytest.fixture(params=['debian', 'centos', 'fedora'])
def tag(request): def tag(request):
''' consumed by image to make the test matrix ''' '''
consumed by image to make the test matrix
'''
return request.param return request.param
@pytest.fixture() @pytest.fixture()
def image(request, tag): def image(request, tag):
''' built by test_000_build_containers.py ''' '''
built by test_000_build_containers.py
'''
return 'pytest_pihole:{}'.format(tag) return 'pytest_pihole:{}'.format(tag)
@pytest.fixture() @pytest.fixture()
def cmd(request): def cmd(request):
''' default to doing nothing by tailing null, but don't exit ''' '''
default to doing nothing by tailing null, but don't exit
'''
return 'tail -f /dev/null' return 'tail -f /dev/null'
# Helper functions
def mock_command(script, args, container):
'''
Allows for setup of commands we don't really want to have to run for real
in unit tests
'''
full_script_path = '/usr/local/bin/{}'.format(script)
mock_script = dedent('''\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1" in'''.format(script=script))
for k, v in args.iteritems():
case = dedent('''
{arg})
echo {res}
exit {retcode}
;;'''.format(arg=k, res=v[0], retcode=v[1]))
mock_script += case
mock_script += dedent('''
esac''')
container.run('''
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}'''.format(script=full_script_path,
content=mock_script,
scriptlog=script))
def mock_command_2(script, args, container):
'''
Allows for setup of commands we don't really want to have to run for real
in unit tests
'''
full_script_path = '/usr/local/bin/{}'.format(script)
mock_script = dedent('''\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1 \$2" in'''.format(script=script))
for k, v in args.iteritems():
case = dedent('''
\"{arg}\")
echo \"{res}\"
exit {retcode}
;;'''.format(arg=k, res=v[0], retcode=v[1]))
mock_script += case
mock_script += dedent('''
esac''')
container.run('''
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}'''.format(script=full_script_path,
content=mock_script,
scriptlog=script))
def run_script(Pihole, script):
result = Pihole.run(script)
assert result.rc == 0
return result

@ -0,0 +1,16 @@
FROM fedora:latest
ENV GITDIR /etc/.pihole
ENV SCRIPTDIR /opt/pihole
RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole
ADD . $GITDIR
RUN cp $GITDIR/advanced/Scripts/*.sh $GITDIR/gravity.sh $GITDIR/pihole $GITDIR/automated\ install/*.sh $SCRIPTDIR/
ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR
RUN true && \
chmod +x $SCRIPTDIR/*
ENV PH_TEST true
#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \

@ -6,10 +6,15 @@ run_local = testinfra.get_backend(
"local://" "local://"
).get_module("Command").run ).get_module("Command").run
@pytest.mark.parametrize("image,tag", [ @pytest.mark.parametrize("image,tag", [
( 'test/debian.Dockerfile', 'pytest_pihole:debian' ), ('test/debian.Dockerfile', 'pytest_pihole:debian'),
( 'test/centos.Dockerfile', 'pytest_pihole:centos' ), ('test/centos.Dockerfile', 'pytest_pihole:centos'),
('test/fedora.Dockerfile', 'pytest_pihole:fedora'),
]) ])
# mark as 'build_stage' so we can ensure images are build first when tests
# are executed in parallel. (not required when tests are executed serially)
@pytest.mark.build_stage
def test_build_pihole_image(image, tag): def test_build_pihole_image(image, tag):
build_cmd = run_local('docker build -f {} -t {} .'.format(image, tag)) build_cmd = run_local('docker build -f {} -t {} .'.format(image, tag))
if build_cmd.rc != 0: if build_cmd.rc != 0:

@ -1,24 +1,40 @@
import pytest
from textwrap import dedent from textwrap import dedent
import re
from conftest import (
SETUPVARS,
tick_box,
info_box,
cross_box,
mock_command,
mock_command_2,
run_script
)
def test_supported_operating_system(Pihole):
'''
confirm installer exists on unsupported distribution
'''
# break supported package managers to emulate an unsupported distribution
Pihole.run('rm -rf /usr/bin/apt-get')
Pihole.run('rm -rf /usr/bin/rpm')
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = cross_box + ' OS distribution not supported'
assert expected_stdout in distro_check.stdout
# assert distro_check.rc == 1
SETUPVARS = {
'PIHOLE_INTERFACE' : 'eth99',
'IPV4_ADDRESS' : '1.1.1.1',
'IPV6_ADDRESS' : 'FE80::240:D0FF:FE48:4672',
'PIHOLE_DNS_1' : '4.2.2.1',
'PIHOLE_DNS_2' : '4.2.2.2'
}
tick_box="[\x1b[1;32m\xe2\x9c\x93\x1b[0m]".decode("utf-8")
cross_box="[\x1b[1;31m\xe2\x9c\x97\x1b[0m]".decode("utf-8")
info_box="[i]".decode("utf-8")
def test_setupVars_are_sourced_to_global_scope(Pihole): def test_setupVars_are_sourced_to_global_scope(Pihole):
''' currently update_dialogs sources setupVars with a dot, '''
currently update_dialogs sources setupVars with a dot,
then various other functions use the variables. then various other functions use the variables.
This confirms the sourced variables are in scope between functions ''' This confirms the sourced variables are in scope between functions
'''
setup_var_file = 'cat <<EOF> /etc/pihole/setupVars.conf\n' setup_var_file = 'cat <<EOF> /etc/pihole/setupVars.conf\n'
for k,v in SETUPVARS.iteritems(): for k, v in SETUPVARS.iteritems():
setup_var_file += "{}={}\n".format(k, v) setup_var_file += "{}={}\n".format(k, v)
setup_var_file += "EOF\n" setup_var_file += "EOF\n"
Pihole.run(setup_var_file) Pihole.run(setup_var_file)
@ -43,13 +59,17 @@ def test_setupVars_are_sourced_to_global_scope(Pihole):
output = run_script(Pihole, script).stdout output = run_script(Pihole, script).stdout
for k,v in SETUPVARS.iteritems(): for k, v in SETUPVARS.iteritems():
assert "{}={}".format(k, v) in output assert "{}={}".format(k, v) in output
def test_setupVars_saved_to_file(Pihole): def test_setupVars_saved_to_file(Pihole):
''' confirm saved settings are written to a file for future updates to re-use ''' '''
set_setup_vars = '\n' # dedent works better with this and padding matching script below confirm saved settings are written to a file for future updates to re-use
for k,v in SETUPVARS.iteritems(): '''
# dedent works better with this and padding matching script below
set_setup_vars = '\n'
for k, v in SETUPVARS.iteritems():
set_setup_vars += " {}={}\n".format(k, v) set_setup_vars += " {}={}\n".format(k, v)
Pihole.run(set_setup_vars).stdout Pihole.run(set_setup_vars).stdout
@ -67,43 +87,57 @@ def test_setupVars_saved_to_file(Pihole):
output = run_script(Pihole, script).stdout output = run_script(Pihole, script).stdout
for k,v in SETUPVARS.iteritems(): for k, v in SETUPVARS.iteritems():
assert "{}={}".format(k, v) in output assert "{}={}".format(k, v) in output
def test_configureFirewall_firewalld_running_no_errors(Pihole): def test_configureFirewall_firewalld_running_no_errors(Pihole):
''' confirms firewalld rules are applied when firewallD is running ''' '''
confirms firewalld rules are applied when firewallD is running
'''
# firewallD returns 'running' as status # firewallD returns 'running' as status
mock_command('firewall-cmd', {'*':('running', 0)}, Pihole) mock_command('firewall-cmd', {'*': ('running', 0)}, Pihole)
# Whiptail dialog returns Ok for user prompt # Whiptail dialog returns Ok for user prompt
mock_command('whiptail', {'*':('', 0)}, Pihole) mock_command('whiptail', {'*': ('', 0)}, Pihole)
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
configureFirewall configureFirewall
''') ''')
expected_stdout = 'Configuring FirewallD for httpd and dnsmasq' expected_stdout = 'Configuring FirewallD for httpd and pihole-FTL'
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
firewall_calls = Pihole.run('cat /var/log/firewall-cmd').stdout firewall_calls = Pihole.run('cat /var/log/firewall-cmd').stdout
assert 'firewall-cmd --state' in firewall_calls assert 'firewall-cmd --state' in firewall_calls
assert 'firewall-cmd --permanent --add-service=http --add-service=dns' in firewall_calls assert ('firewall-cmd '
'--permanent '
'--add-service=http '
'--add-service=dns') in firewall_calls
assert 'firewall-cmd --reload' in firewall_calls assert 'firewall-cmd --reload' in firewall_calls
def test_configureFirewall_firewalld_disabled_no_errors(Pihole): def test_configureFirewall_firewalld_disabled_no_errors(Pihole):
''' confirms firewalld rules are not applied when firewallD is not running ''' '''
confirms firewalld rules are not applied when firewallD is not running
'''
# firewallD returns non-running status # firewallD returns non-running status
mock_command('firewall-cmd', {'*':('not running', '1')}, Pihole) mock_command('firewall-cmd', {'*': ('not running', '1')}, Pihole)
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
configureFirewall configureFirewall
''') ''')
expected_stdout = 'No active firewall detected.. skipping firewall configuration' expected_stdout = ('No active firewall detected.. '
'skipping firewall configuration')
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
def test_configureFirewall_firewalld_enabled_declined_no_errors(Pihole): def test_configureFirewall_firewalld_enabled_declined_no_errors(Pihole):
''' confirms firewalld rules are not applied when firewallD is running, user declines ruleset ''' '''
confirms firewalld rules are not applied when firewallD is running, user
declines ruleset
'''
# firewallD returns running status # firewallD returns running status
mock_command('firewall-cmd', {'*':('running', 0)}, Pihole) mock_command('firewall-cmd', {'*': ('running', 0)}, Pihole)
# Whiptail dialog returns Cancel for user prompt # Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*':('', 1)}, Pihole) mock_command('whiptail', {'*': ('', 1)}, Pihole)
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
configureFirewall configureFirewall
@ -111,6 +145,7 @@ def test_configureFirewall_firewalld_enabled_declined_no_errors(Pihole):
expected_stdout = 'Not installing firewall rulesets.' expected_stdout = 'Not installing firewall rulesets.'
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
def test_configureFirewall_no_firewall(Pihole): def test_configureFirewall_no_firewall(Pihole):
''' confirms firewall skipped no daemon is running ''' ''' confirms firewall skipped no daemon is running '''
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
@ -120,14 +155,18 @@ def test_configureFirewall_no_firewall(Pihole):
expected_stdout = 'No active firewall detected' expected_stdout = 'No active firewall detected'
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
def test_configureFirewall_IPTables_enabled_declined_no_errors(Pihole): def test_configureFirewall_IPTables_enabled_declined_no_errors(Pihole):
''' confirms IPTables rules are not applied when IPTables is running, user declines ruleset ''' '''
confirms IPTables rules are not applied when IPTables is running, user
declines ruleset
'''
# iptables command exists # iptables command exists
mock_command('iptables', {'*':('', '0')}, Pihole) mock_command('iptables', {'*': ('', '0')}, Pihole)
# modinfo returns always true (ip_tables module check) # modinfo returns always true (ip_tables module check)
mock_command('modinfo', {'*':('', '0')}, Pihole) mock_command('modinfo', {'*': ('', '0')}, Pihole)
# Whiptail dialog returns Cancel for user prompt # Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*':('', '1')}, Pihole) mock_command('whiptail', {'*': ('', '1')}, Pihole)
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
configureFirewall configureFirewall
@ -135,14 +174,19 @@ def test_configureFirewall_IPTables_enabled_declined_no_errors(Pihole):
expected_stdout = 'Not installing firewall rulesets.' expected_stdout = 'Not installing firewall rulesets.'
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
def test_configureFirewall_IPTables_enabled_rules_exist_no_errors(Pihole): def test_configureFirewall_IPTables_enabled_rules_exist_no_errors(Pihole):
''' confirms IPTables rules are not applied when IPTables is running and rules exist ''' '''
# iptables command exists and returns 0 on calls (should return 0 on iptables -C) confirms IPTables rules are not applied when IPTables is running and rules
mock_command('iptables', {'-S':('-P INPUT DENY', '0')}, Pihole) exist
'''
# iptables command exists and returns 0 on calls
# (should return 0 on iptables -C)
mock_command('iptables', {'-S': ('-P INPUT DENY', '0')}, Pihole)
# modinfo returns always true (ip_tables module check) # modinfo returns always true (ip_tables module check)
mock_command('modinfo', {'*':('', '0')}, Pihole) mock_command('modinfo', {'*': ('', '0')}, Pihole)
# Whiptail dialog returns Cancel for user prompt # Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*':('', '0')}, Pihole) mock_command('whiptail', {'*': ('', '0')}, Pihole)
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
configureFirewall configureFirewall
@ -150,18 +194,46 @@ def test_configureFirewall_IPTables_enabled_rules_exist_no_errors(Pihole):
expected_stdout = 'Installing new IPTables firewall rulesets' expected_stdout = 'Installing new IPTables firewall rulesets'
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
firewall_calls = Pihole.run('cat /var/log/iptables').stdout firewall_calls = Pihole.run('cat /var/log/iptables').stdout
assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 80 -j ACCEPT' not in firewall_calls # General call type occurances
assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT' not in firewall_calls assert len(re.findall(r'iptables -S', firewall_calls)) == 1
assert 'iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT' not in firewall_calls assert len(re.findall(r'iptables -C', firewall_calls)) == 4
assert len(re.findall(r'iptables -I', firewall_calls)) == 0
# Specific port call occurances
assert len(re.findall(r'tcp --dport 80', firewall_calls)) == 1
assert len(re.findall(r'tcp --dport 53', firewall_calls)) == 1
assert len(re.findall(r'udp --dport 53', firewall_calls)) == 1
assert len(re.findall(r'tcp --dport 4711:4720', firewall_calls)) == 1
def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole): def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole):
''' confirms IPTables rules are applied when IPTables is running and rules do not exist ''' '''
confirms IPTables rules are applied when IPTables is running and rules do
not exist
'''
# iptables command and returns 0 on calls (should return 1 on iptables -C) # iptables command and returns 0 on calls (should return 1 on iptables -C)
mock_command('iptables', {'-S':('-P INPUT DENY', '0'), '-C':('', 1), '-I':('', 0)}, Pihole) mock_command(
'iptables',
{
'-S': (
'-P INPUT DENY',
'0'
),
'-C': (
'',
1
),
'-I': (
'',
0
)
},
Pihole
)
# modinfo returns always true (ip_tables module check) # modinfo returns always true (ip_tables module check)
mock_command('modinfo', {'*':('', '0')}, Pihole) mock_command('modinfo', {'*': ('', '0')}, Pihole)
# Whiptail dialog returns Cancel for user prompt # Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*':('', '0')}, Pihole) mock_command('whiptail', {'*': ('', '0')}, Pihole)
configureFirewall = Pihole.run(''' configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
configureFirewall configureFirewall
@ -169,52 +241,160 @@ def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole):
expected_stdout = 'Installing new IPTables firewall rulesets' expected_stdout = 'Installing new IPTables firewall rulesets'
assert expected_stdout in configureFirewall.stdout assert expected_stdout in configureFirewall.stdout
firewall_calls = Pihole.run('cat /var/log/iptables').stdout firewall_calls = Pihole.run('cat /var/log/iptables').stdout
assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 80 -j ACCEPT' in firewall_calls # General call type occurances
assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT' in firewall_calls assert len(re.findall(r'iptables -S', firewall_calls)) == 1
assert 'iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT' in firewall_calls assert len(re.findall(r'iptables -C', firewall_calls)) == 4
assert len(re.findall(r'iptables -I', firewall_calls)) == 4
# Specific port call occurances
assert len(re.findall(r'tcp --dport 80', firewall_calls)) == 2
assert len(re.findall(r'tcp --dport 53', firewall_calls)) == 2
assert len(re.findall(r'udp --dport 53', firewall_calls)) == 2
assert len(re.findall(r'tcp --dport 4711:4720', firewall_calls)) == 2
def test_selinux_enforcing_default_exit(Pihole):
'''
confirms installer prompts to exit when SELinux is Enforcing by default
'''
# getenforce returns the running state of SELinux
mock_command('getenforce', {'*': ('Enforcing', '0')}, Pihole)
# Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*': ('', '1')}, Pihole)
check_selinux = Pihole.run('''
source /opt/pihole/basic-install.sh
checkSelinux
''')
expected_stdout = info_box + ' SELinux mode detected: Enforcing'
assert expected_stdout in check_selinux.stdout
expected_stdout = 'SELinux Enforcing detected, exiting installer'
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 1
def test_selinux_enforcing_continue(Pihole):
'''
confirms installer prompts to continue with custom policy warning
'''
# getenforce returns the running state of SELinux
mock_command('getenforce', {'*': ('Enforcing', '0')}, Pihole)
# Whiptail dialog returns Continue for user prompt
mock_command('whiptail', {'*': ('', '0')}, Pihole)
check_selinux = Pihole.run('''
source /opt/pihole/basic-install.sh
checkSelinux
''')
expected_stdout = info_box + ' SELinux mode detected: Enforcing'
assert expected_stdout in check_selinux.stdout
expected_stdout = info_box + (' Continuing installation with SELinux '
'Enforcing')
assert expected_stdout in check_selinux.stdout
expected_stdout = info_box + (' Please refer to official SELinux '
'documentation to create a custom policy')
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 0
def test_selinux_permissive(Pihole):
'''
confirms installer continues when SELinux is Permissive
'''
# getenforce returns the running state of SELinux
mock_command('getenforce', {'*': ('Permissive', '0')}, Pihole)
check_selinux = Pihole.run('''
source /opt/pihole/basic-install.sh
checkSelinux
''')
expected_stdout = info_box + ' SELinux mode detected: Permissive'
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 0
def test_selinux_disabled(Pihole):
'''
confirms installer continues when SELinux is Disabled
'''
mock_command('getenforce', {'*': ('Disabled', '0')}, Pihole)
check_selinux = Pihole.run('''
source /opt/pihole/basic-install.sh
checkSelinux
''')
expected_stdout = info_box + ' SELinux mode detected: Disabled'
assert expected_stdout in check_selinux.stdout
assert check_selinux.rc == 0
def test_installPiholeWeb_fresh_install_no_errors(Pihole): def test_installPiholeWeb_fresh_install_no_errors(Pihole):
''' confirms all web page assets from Core repo are installed on a fresh build ''' '''
confirms all web page assets from Core repo are installed on a fresh build
'''
installWeb = Pihole.run(''' installWeb = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
installPiholeWeb installPiholeWeb
''') ''')
assert info_box + ' Installing blocking page...' in installWeb.stdout expected_stdout = info_box + ' Installing blocking page...'
assert tick_box + ' Creating directory for blocking page, and copying files' in installWeb.stdout assert expected_stdout in installWeb.stdout
assert cross_box + ' Backing up index.lighttpd.html' in installWeb.stdout expected_stdout = tick_box + (' Creating directory for blocking page, '
assert 'No default index.lighttpd.html file found... not backing up' in installWeb.stdout 'and copying files')
assert tick_box + ' Installing sudoer file' in installWeb.stdout assert expected_stdout in installWeb.stdout
expected_stdout = cross_box + ' Backing up index.lighttpd.html'
assert expected_stdout in installWeb.stdout
expected_stdout = ('No default index.lighttpd.html file found... '
'not backing up')
assert expected_stdout in installWeb.stdout
expected_stdout = tick_box + ' Installing sudoer file'
assert expected_stdout in installWeb.stdout
web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout
assert 'index.php' in web_directory assert 'index.php' in web_directory
assert 'blockingpage.css' in web_directory assert 'blockingpage.css' in web_directory
def test_update_package_cache_success_no_errors(Pihole): def test_update_package_cache_success_no_errors(Pihole):
''' confirms package cache was updated without any errors''' '''
confirms package cache was updated without any errors
'''
updateCache = Pihole.run(''' updateCache = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
distro_check distro_check
update_package_cache update_package_cache
''') ''')
assert tick_box + ' Update local cache of available packages' in updateCache.stdout expected_stdout = tick_box + ' Update local cache of available packages'
assert 'Error: Unable to update package cache.' not in updateCache.stdout assert expected_stdout in updateCache.stdout
assert 'error' not in updateCache.stdout.lower()
def test_update_package_cache_failure_no_errors(Pihole): def test_update_package_cache_failure_no_errors(Pihole):
''' confirms package cache was not updated''' '''
mock_command('apt-get', {'update':('', '1')}, Pihole) confirms package cache was not updated
'''
mock_command('apt-get', {'update': ('', '1')}, Pihole)
updateCache = Pihole.run(''' updateCache = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
distro_check distro_check
update_package_cache update_package_cache
''') ''')
assert cross_box + ' Update local cache of available packages' in updateCache.stdout expected_stdout = cross_box + ' Update local cache of available packages'
assert expected_stdout in updateCache.stdout
assert 'Error: Unable to update package cache.' in updateCache.stdout assert 'Error: Unable to update package cache.' in updateCache.stdout
def test_FTL_detect_aarch64_no_errors(Pihole): def test_FTL_detect_aarch64_no_errors(Pihole):
''' confirms only aarch64 package is downloaded for FTL engine ''' '''
confirms only aarch64 package is downloaded for FTL engine
'''
# mock uname to return aarch64 platform # mock uname to return aarch64 platform
mock_command('uname', {'-m':('aarch64', '0')}, Pihole) mock_command('uname', {'-m': ('aarch64', '0')}, Pihole)
# mock ldd to respond with aarch64 shared library # mock ldd to respond with aarch64 shared library
mock_command('ldd', {'/bin/ls':('/lib/ld-linux-aarch64.so.1', '0')}, Pihole) mock_command(
'ldd',
{
'/bin/ls': (
'/lib/ld-linux-aarch64.so.1',
'0'
)
},
Pihole
)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
FTLdetect FTLdetect
@ -226,29 +406,36 @@ def test_FTL_detect_aarch64_no_errors(Pihole):
expected_stdout = tick_box + ' Downloading and Installing FTL' expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_FTL_detect_armv6l_no_errors(Pihole): def test_FTL_detect_armv6l_no_errors(Pihole):
''' confirms only armv6l package is downloaded for FTL engine ''' '''
confirms only armv6l package is downloaded for FTL engine
'''
# mock uname to return armv6l platform # mock uname to return armv6l platform
mock_command('uname', {'-m':('armv6l', '0')}, Pihole) mock_command('uname', {'-m': ('armv6l', '0')}, Pihole)
# mock ldd to respond with aarch64 shared library # mock ldd to respond with aarch64 shared library
mock_command('ldd', {'/bin/ls':('/lib/ld-linux-armhf.so.3', '0')}, Pihole) mock_command('ldd', {'/bin/ls': ('/lib/ld-linux-armhf.so.3', '0')}, Pihole)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
FTLdetect FTLdetect
''') ''')
expected_stdout = info_box + ' FTL Checks...' expected_stdout = info_box + ' FTL Checks...'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
expected_stdout = tick_box + ' Detected ARM-hf architecture (armv6 or lower)' expected_stdout = tick_box + (' Detected ARM-hf architecture '
'(armv6 or lower)')
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
expected_stdout = tick_box + ' Downloading and Installing FTL' expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_FTL_detect_armv7l_no_errors(Pihole): def test_FTL_detect_armv7l_no_errors(Pihole):
''' confirms only armv7l package is downloaded for FTL engine ''' '''
confirms only armv7l package is downloaded for FTL engine
'''
# mock uname to return armv7l platform # mock uname to return armv7l platform
mock_command('uname', {'-m':('armv7l', '0')}, Pihole) mock_command('uname', {'-m': ('armv7l', '0')}, Pihole)
# mock ldd to respond with aarch64 shared library # mock ldd to respond with aarch64 shared library
mock_command('ldd', {'/bin/ls':('/lib/ld-linux-armhf.so.3', '0')}, Pihole) mock_command('ldd', {'/bin/ls': ('/lib/ld-linux-armhf.so.3', '0')}, Pihole)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
FTLdetect FTLdetect
@ -260,8 +447,11 @@ def test_FTL_detect_armv7l_no_errors(Pihole):
expected_stdout = tick_box + ' Downloading and Installing FTL' expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_FTL_detect_x86_64_no_errors(Pihole): def test_FTL_detect_x86_64_no_errors(Pihole):
''' confirms only x86_64 package is downloaded for FTL engine ''' '''
confirms only x86_64 package is downloaded for FTL engine
'''
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
FTLdetect FTLdetect
@ -273,10 +463,11 @@ def test_FTL_detect_x86_64_no_errors(Pihole):
expected_stdout = tick_box + ' Downloading and Installing FTL' expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_FTL_detect_unknown_no_errors(Pihole): def test_FTL_detect_unknown_no_errors(Pihole):
''' confirms only generic package is downloaded for FTL engine ''' ''' confirms only generic package is downloaded for FTL engine '''
# mock uname to return generic platform # mock uname to return generic platform
mock_command('uname', {'-m':('mips', '0')}, Pihole) mock_command('uname', {'-m': ('mips', '0')}, Pihole)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
FTLdetect FTLdetect
@ -284,8 +475,11 @@ def test_FTL_detect_unknown_no_errors(Pihole):
expected_stdout = 'Not able to detect architecture (unknown: mips)' expected_stdout = 'Not able to detect architecture (unknown: mips)'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_FTL_download_aarch64_no_errors(Pihole): def test_FTL_download_aarch64_no_errors(Pihole):
''' confirms only aarch64 package is downloaded for FTL engine ''' '''
confirms only aarch64 package is downloaded for FTL engine
'''
# mock uname to return generic platform # mock uname to return generic platform
download_binary = Pihole.run(''' download_binary = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
@ -293,13 +487,13 @@ def test_FTL_download_aarch64_no_errors(Pihole):
''') ''')
expected_stdout = tick_box + ' Downloading and Installing FTL' expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in download_binary.stdout assert expected_stdout in download_binary.stdout
error = 'Error: Download of binary from Github failed' assert 'error' not in download_binary.stdout.lower()
assert error not in download_binary.stdout
error = 'Error: URL not found'
assert error not in download_binary.stdout
def test_FTL_download_unknown_fails_no_errors(Pihole): def test_FTL_download_unknown_fails_no_errors(Pihole):
''' confirms unknown binary is not downloaded for FTL engine ''' '''
confirms unknown binary is not downloaded for FTL engine
'''
# mock uname to return generic platform # mock uname to return generic platform
download_binary = Pihole.run(''' download_binary = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
@ -310,8 +504,11 @@ def test_FTL_download_unknown_fails_no_errors(Pihole):
error = 'Error: URL not found' error = 'Error: URL not found'
assert error in download_binary.stdout assert error in download_binary.stdout
def test_FTL_binary_installed_and_responsive_no_errors(Pihole): def test_FTL_binary_installed_and_responsive_no_errors(Pihole):
''' confirms FTL binary is copied and functional in installed location ''' '''
confirms FTL binary is copied and functional in installed location
'''
installed_binary = Pihole.run(''' installed_binary = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
FTLdetect FTLdetect
@ -320,8 +517,11 @@ def test_FTL_binary_installed_and_responsive_no_errors(Pihole):
expected_stdout = 'v' expected_stdout = 'v'
assert expected_stdout in installed_binary.stdout assert expected_stdout in installed_binary.stdout
# def test_FTL_support_files_installed(Pihole): # def test_FTL_support_files_installed(Pihole):
# ''' confirms FTL support files are installed ''' # '''
# confirms FTL support files are installed
# '''
# support_files = Pihole.run(''' # support_files = Pihole.run('''
# source /opt/pihole/basic-install.sh # source /opt/pihole/basic-install.sh
# FTLdetect # FTLdetect
@ -334,21 +534,46 @@ def test_FTL_binary_installed_and_responsive_no_errors(Pihole):
# assert '644 /run/pihole-FTL.pid' in support_files.stdout # assert '644 /run/pihole-FTL.pid' in support_files.stdout
# assert '644 /var/log/pihole-FTL.log' in support_files.stdout # assert '644 /var/log/pihole-FTL.log' in support_files.stdout
def test_IPv6_only_link_local(Pihole): def test_IPv6_only_link_local(Pihole):
''' confirms IPv6 blocking is disabled for Link-local address ''' '''
confirms IPv6 blocking is disabled for Link-local address
'''
# mock ip -6 address to return Link-local address # mock ip -6 address to return Link-local address
mock_command_2('ip', {'-6 address':('inet6 fe80::d210:52fa:fe00:7ad7/64 scope link', '0')}, Pihole) mock_command_2(
'ip',
{
'-6 address': (
'inet6 fe80::d210:52fa:fe00:7ad7/64 scope link',
'0'
)
},
Pihole
)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
useIPv6dialog useIPv6dialog
''') ''')
expected_stdout = 'Unable to find IPv6 ULA/GUA address, IPv6 adblocking will not be enabled' expected_stdout = ('Unable to find IPv6 ULA/GUA address, '
'IPv6 adblocking will not be enabled')
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_IPv6_only_ULA(Pihole): def test_IPv6_only_ULA(Pihole):
''' confirms IPv6 blocking is enabled for ULA addresses ''' '''
confirms IPv6 blocking is enabled for ULA addresses
'''
# mock ip -6 address to return ULA address # mock ip -6 address to return ULA address
mock_command_2('ip', {'-6 address':('inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) mock_command_2(
'ip',
{
'-6 address': (
'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global',
'0'
)
},
Pihole
)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
useIPv6dialog useIPv6dialog
@ -356,10 +581,22 @@ def test_IPv6_only_ULA(Pihole):
expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_IPv6_only_GUA(Pihole): def test_IPv6_only_GUA(Pihole):
''' confirms IPv6 blocking is enabled for GUA addresses ''' '''
confirms IPv6 blocking is enabled for GUA addresses
'''
# mock ip -6 address to return GUA address # mock ip -6 address to return GUA address
mock_command_2('ip', {'-6 address':('inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) mock_command_2(
'ip',
{
'-6 address': (
'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global',
'0'
)
},
Pihole
)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
useIPv6dialog useIPv6dialog
@ -367,10 +604,23 @@ def test_IPv6_only_GUA(Pihole):
expected_stdout = 'Found IPv6 GUA address, using it for blocking IPv6 ads' expected_stdout = 'Found IPv6 GUA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_IPv6_GUA_ULA_test(Pihole): def test_IPv6_GUA_ULA_test(Pihole):
''' confirms IPv6 blocking is enabled for GUA and ULA addresses ''' '''
confirms IPv6 blocking is enabled for GUA and ULA addresses
'''
# mock ip -6 address to return GUA and ULA addresses # mock ip -6 address to return GUA and ULA addresses
mock_command_2('ip', {'-6 address':('inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\ninet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) mock_command_2(
'ip',
{
'-6 address': (
'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\n'
'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global',
'0'
)
},
Pihole
)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
useIPv6dialog useIPv6dialog
@ -378,61 +628,26 @@ def test_IPv6_GUA_ULA_test(Pihole):
expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
def test_IPv6_ULA_GUA_test(Pihole): def test_IPv6_ULA_GUA_test(Pihole):
''' confirms IPv6 blocking is enabled for GUA and ULA addresses ''' '''
confirms IPv6 blocking is enabled for GUA and ULA addresses
'''
# mock ip -6 address to return ULA and GUA addresses # mock ip -6 address to return ULA and GUA addresses
mock_command_2('ip', {'-6 address':('inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\ninet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global', '0')}, Pihole) mock_command_2(
'ip',
{
'-6 address': (
'inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\n'
'inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global',
'0'
)
},
Pihole
)
detectPlatform = Pihole.run(''' detectPlatform = Pihole.run('''
source /opt/pihole/basic-install.sh source /opt/pihole/basic-install.sh
useIPv6dialog useIPv6dialog
''') ''')
expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout assert expected_stdout in detectPlatform.stdout
# Helper functions
def mock_command(script, args, container):
''' Allows for setup of commands we don't really want to have to run for real in unit tests '''
full_script_path = '/usr/local/bin/{}'.format(script)
mock_script = dedent('''\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1" in'''.format(script=script))
for k, v in args.iteritems():
case = dedent('''
{arg})
echo {res}
exit {retcode}
;;'''.format(arg=k, res=v[0], retcode=v[1]))
mock_script += case
mock_script += dedent('''
esac''')
container.run('''
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}'''.format(script=full_script_path, content=mock_script, scriptlog=script))
def mock_command_2(script, args, container):
''' Allows for setup of commands we don't really want to have to run for real in unit tests '''
full_script_path = '/usr/local/bin/{}'.format(script)
mock_script = dedent('''\
#!/bin/bash -e
echo "\$0 \$@" >> /var/log/{script}
case "\$1 \$2" in'''.format(script=script))
for k, v in args.iteritems():
case = dedent('''
\"{arg}\")
echo \"{res}\"
exit {retcode}
;;'''.format(arg=k, res=v[0], retcode=v[1]))
mock_script += case
mock_script += dedent('''
esac''')
container.run('''
cat <<EOF> {script}\n{content}\nEOF
chmod +x {script}
rm -f /var/log/{scriptlog}'''.format(script=full_script_path, content=mock_script, scriptlog=script))
def run_script(Pihole, script):
result = Pihole.run(script)
assert result.rc == 0
return result

@ -0,0 +1,209 @@
import pytest
from conftest import (
tick_box,
info_box,
cross_box,
mock_command,
mock_command_2,
)
@pytest.mark.parametrize("tag", [('fedora'), ])
def test_epel_and_remi_not_installed_fedora(Pihole):
'''
confirms installer does not attempt to install EPEL/REMI repositories
on Fedora
'''
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
assert distro_check.stdout == ''
epel_package = Pihole.package('epel-release')
assert not epel_package.is_installed
remi_package = Pihole.package('remi-release')
assert not remi_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_release_supported_version_check_centos(Pihole):
'''
confirms installer exits on unsupported releases of CentOS
'''
# mock CentOS release < 7 (unsupported)
mock_command_2(
'rpm',
{"-q --queryformat '%{VERSION}' centos-release'": (
'5',
'0'
)},
Pihole
)
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = cross_box + (' CentOS is not suported.')
assert expected_stdout in distro_check.stdout
expected_stdout = 'Please update to CentOS release 7 or later'
assert expected_stdout in distro_check.stdout
@pytest.mark.parametrize("tag", [('centos'), ])
def test_enable_epel_repository_centos(Pihole):
'''
confirms the EPEL package repository is enabled when installed on CentOS
'''
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = info_box + (' Enabling EPEL package repository '
'(https://fedoraproject.org/wiki/EPEL)')
assert expected_stdout in distro_check.stdout
expected_stdout = tick_box + ' Installed epel-release'
assert expected_stdout in distro_check.stdout
epel_package = Pihole.package('epel-release')
assert epel_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_php_upgrade_default_optout_centos(Pihole):
'''
confirms the default behavior to opt-out of installing PHP7 from REMI
'''
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. '
'Deprecated PHP may be in use.')
assert expected_stdout in distro_check.stdout
remi_package = Pihole.package('remi-release')
assert not remi_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_php_upgrade_user_optout_centos(Pihole):
'''
confirms installer behavior when user opt-out of installing PHP7 from REMI
(php not currently installed)
'''
# Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*': ('', '1')}, Pihole)
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. '
'Deprecated PHP may be in use.')
assert expected_stdout in distro_check.stdout
remi_package = Pihole.package('remi-release')
assert not remi_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_php_upgrade_user_optin_centos(Pihole):
'''
confirms installer behavior when user opt-in to installing PHP7 from REMI
(php not currently installed)
'''
# Whiptail dialog returns Continue for user prompt
mock_command('whiptail', {'*': ('', '0')}, Pihole)
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
assert 'opt-out' not in distro_check.stdout
expected_stdout = info_box + (' Enabling Remi\'s RPM repository '
'(https://rpms.remirepo.net)')
assert expected_stdout in distro_check.stdout
expected_stdout = tick_box + (' Remi\'s RPM repository has '
'been enabled for PHP7')
assert expected_stdout in distro_check.stdout
remi_package = Pihole.package('remi-release')
assert remi_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_php_version_lt_7_detected_upgrade_default_optout_centos(Pihole):
'''
confirms the default behavior to opt-out of upgrading to PHP7 from REMI
'''
# first we will install the default php version to test installer behavior
php_install = Pihole.run('yum install -y php')
assert php_install.rc == 0
php_package = Pihole.package('php')
default_centos_php_version = php_package.version.split('.')[0]
if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended
pytest.skip("Test deprecated . Detected default PHP version >= 7")
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. '
'Deprecated PHP may be in use.')
assert expected_stdout in distro_check.stdout
remi_package = Pihole.package('remi-release')
assert not remi_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_php_version_lt_7_detected_upgrade_user_optout_centos(Pihole):
'''
confirms installer behavior when user opt-out to upgrade to PHP7 via REMI
'''
# first we will install the default php version to test installer behavior
php_install = Pihole.run('yum install -y php')
assert php_install.rc == 0
php_package = Pihole.package('php')
default_centos_php_version = php_package.version.split('.')[0]
if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended
pytest.skip("Test deprecated . Detected default PHP version >= 7")
# Whiptail dialog returns Cancel for user prompt
mock_command('whiptail', {'*': ('', '1')}, Pihole)
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
''')
expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. '
'Deprecated PHP may be in use.')
assert expected_stdout in distro_check.stdout
remi_package = Pihole.package('remi-release')
assert not remi_package.is_installed
@pytest.mark.parametrize("tag", [('centos'), ])
def test_php_version_lt_7_detected_upgrade_user_optin_centos(Pihole):
'''
confirms installer behavior when user opt-in to upgrade to PHP7 via REMI
'''
# first we will install the default php version to test installer behavior
php_install = Pihole.run('yum install -y php')
assert php_install.rc == 0
php_package = Pihole.package('php')
default_centos_php_version = php_package.version.split('.')[0]
if int(default_centos_php_version) >= 7: # PHP7 is supported/recommended
pytest.skip("Test deprecated . Detected default PHP version >= 7")
# Whiptail dialog returns Continue for user prompt
mock_command('whiptail', {'*': ('', '0')}, Pihole)
distro_check = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
install_dependent_packages PIHOLE_WEB_DEPS[@]
''')
expected_stdout = info_box + (' User opt-out of PHP 7 upgrade on CentOS. '
'Deprecated PHP may be in use.')
assert expected_stdout not in distro_check.stdout
expected_stdout = info_box + (' Enabling Remi\'s RPM repository '
'(https://rpms.remirepo.net)')
assert expected_stdout in distro_check.stdout
expected_stdout = tick_box + (' Remi\'s RPM repository has '
'been enabled for PHP7')
assert expected_stdout in distro_check.stdout
remi_package = Pihole.package('remi-release')
assert remi_package.is_installed
updated_php_package = Pihole.package('php')
updated_php_version = updated_php_package.version.split('.')[0]
assert int(updated_php_version) == 7

@ -1,13 +1,18 @@
import pytest
import testinfra import testinfra
run_local = testinfra.get_backend( run_local = testinfra.get_backend(
"local://" "local://"
).get_module("Command").run ).get_module("Command").run
def test_scripts_pass_shellcheck(): def test_scripts_pass_shellcheck():
''' Make sure shellcheck does not find anything wrong with our shell scripts ''' '''
shellcheck = "find . -type f -name 'update.sh' | while read file; do shellcheck -x \"$file\" -e SC1090,SC1091; done;" Make sure shellcheck does not find anything wrong with our shell scripts
'''
shellcheck = ("find . -type f -name 'update.sh' "
"| while read file; do "
"shellcheck -x \"$file\" -e SC1090,SC1091; "
"done;")
results = run_local(shellcheck) results = run_local(shellcheck)
print results.stdout print results.stdout
assert '' == results.stdout assert '' == results.stdout

@ -0,0 +1,10 @@
[tox]
envlist = py27
[testenv]
whitelist_externals = docker
deps = -rrequirements.txt
commands = docker build -f test/debian.Dockerfile -t pytest_pihole:debian .
docker build -f test/centos.Dockerfile -t pytest_pihole:centos .
docker build -f test/fedora.Dockerfile -t pytest_pihole:fedora .
pytest {posargs:-vv -n auto} -m "not build_stage" ./test/
Loading…
Cancel
Save