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
indent_style = space
indent_size = tab
tab_width = 2
tab_width = 4
charset = utf-8
trim_trailing_whitespace = true

2
.github/dco.yml vendored

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

5
.gitignore vendored

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

@ -1,11 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<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>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>

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

@ -7,4 +7,6 @@ python:
install:
- 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
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/>
</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)
- **Resolute**: content is blocked in _non-browser locations_, such as ad-laden mobile apps and smart TVs
@ -60,16 +60,21 @@ Make no mistake: **your support is absolutely vital to help keep us innovating!*
### Donations
Sending a donation using our links below is **extremely helpful** in offsetting a portion of our monthly expenses:
&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/>
&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/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/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
If you'd rather not donate (_which is okay!_), there are other ways you can help support us:
- [Digital Ocean](http://www.digitalocean.com/?refcode=344d234950e1) affiliate link
- [Vultr](http://www.vultr.com/?ref=7190426) affiliate link
- [UNIXstickers.com](http://unixstickers.refr.cc/jacobs) affiliate link
- [Pi-hole Swag Store](https://pi-hole.net/shop/)
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_
- [UNIXstickers.com](http://unixstickers.refr.cc/jacobs) _save $5 when you spend $9 using our affiliate link_
- [Pi-hole Swag Store](https://pi-hole.net/shop/) _affiliate link_
- [Amazon](http://www.amazon.com/exec/obidos/redirect-home/pihole09-20) _affiliate link_
- [DNS Made Easy](https://cp.dnsmadeeasy.com/u/133706) _affiliate link_
- [Vultr](http://www.vultr.com/?ref=7190426) _affiliate link_
- Spreading the word about our software, and how you have benefited from it
### 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://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>
</ul>
<br/>
<ul>
<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://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
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:
* 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):
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)
## The Faster-Than-Light Engine
The [FTL Engine](https://github.com/pi-hole/FTL) is a lightweight, purpose-built daemon used to provide statistics needed for the Web Interface, and its API can be easily integrated into your own projects. As the name implies, FTL does this all *very quickly*!
## Faster-than-light Engine
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:
* 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
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
- [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/)
- [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/)
@ -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: 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)
-----
## 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

@ -179,6 +179,7 @@ get_init_stats() {
90009[2-3]|920093) sys_model=" Zero";; # 512MB
9000c1) sys_model=" Zero W";; # 512MB
a02082|a[2-3]2082) sys_model=" 3, Model B";; # 1GB
a020d3) sys_model=" 3, Model B+";; # 1GB
*) sys_model="";;
esac
sys_type="Raspberry Pi$sys_model"
@ -477,10 +478,7 @@ chronoFunc() {
${COL_LIGHT_RED}Press Ctrl-C to exit${COL_NC}
${COL_DARK_GRAY}$scr_line_str${COL_NC}"
else
echo -e "|¯¯¯(¯)_|¯|_ ___|¯|___$phc_ver_str
| ¯_/¯|_| ' \\/ _ \\ / -_)$lte_ver_str
|_| |_| |_||_\\___/_\\___|$ftl_ver_str
${COL_DARK_GRAY}$scr_line_str${COL_NC}"
echo -e "|¯¯¯(¯)_|¯|_ ___|¯|___$phc_ver_str| ¯_/¯|_| ' \\/ _ \\ / -_)$lte_ver_str|_| |_| |_||_\\___/_\\___|$ftl_ver_str ${COL_DARK_GRAY}$scr_line_str${COL_NC}"
fi
printFunc " Hostname: " "$sys_name" "$host_info"

@ -13,10 +13,11 @@ basename=pihole
piholeDir=/etc/"${basename}"
whitelist="${piholeDir}"/whitelist.txt
blacklist="${piholeDir}"/blacklist.txt
readonly wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf"
readonly regexlist="/etc/pihole/regex.list"
reload=false
addmode=true
verbose=true
wildcard=false
domList=()
@ -31,9 +32,12 @@ helpFunc() {
if [[ "${listMain}" == "${whitelist}" ]]; then
param="w"
type="white"
elif [[ "${listMain}" == "${wildcardlist}" ]]; then
param="wild"
elif [[ "${listMain}" == "${regexlist}" && "${wildcard}" == true ]]; then
param="-wild"
type="wildcard black"
elif [[ "${listMain}" == "${regexlist}" ]]; then
param="-regex"
type="regex black"
else
param="b"
type="black"
@ -57,7 +61,8 @@ Options:
EscapeRegexp() {
# This way we may safely insert an arbitrary
# string in our regular expressions
# Also remove leading "." if present
# This sed is intentionally executed in three steps to ease maintainability
# The first sed removes any amount of leading dots
echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g"
}
@ -65,10 +70,14 @@ HandleOther() {
# Convert to lowercase
domain="${1,,}"
# Check validity of domain
# Check validity of domain (don't check for regex entries)
if [[ "${#domain}" -le 253 ]]; then
validDomain=$(grep -P "^((-|_)*[a-z\d]((-|_)*[a-z\d])*(-|_)*)(\.(-|_)*([a-z\d]((-|_)*[a-z\d])*))*$" <<< "${domain}") # Valid chars check
validDomain=$(grep -P "^[^\.]{1,63}(\.[^\.]{1,63})*$" <<< "${validDomain}") # Length of each label
if [[ "${listMain}" == "${regexlist}" && "${wildcard}" == false ]]; then
validDomain="${domain}"
else
validDomain=$(grep -P "^((-|_)*[a-z\\d]((-|_)*[a-z\\d])*(-|_)*)(\\.(-|_)*([a-z\\d]((-|_)*[a-z\\d])*))*$" <<< "${domain}") # Valid chars check
validDomain=$(grep -P "^[^\\.]{1,63}(\\.[^\\.]{1,63})*$" <<< "${validDomain}") # Length of each label
fi
fi
if [[ -n "${validDomain}" ]]; then
@ -94,9 +103,6 @@ PoplistFile() {
if ${addmode}; then
AddDomain "${dom}" "${listMain}"
RemoveDomain "${dom}" "${listAlt}"
if [[ "${listMain}" == "${whitelist}" || "${listMain}" == "${blacklist}" ]]; then
RemoveDomain "${dom}" "${wildcardlist}"
fi
else
RemoveDomain "${dom}" "${listMain}"
fi
@ -109,7 +115,6 @@ AddDomain() {
[[ "${list}" == "${whitelist}" ]] && listname="whitelist"
[[ "${list}" == "${blacklist}" ]] && listname="blacklist"
[[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist"
if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then
[[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only"
@ -121,7 +126,7 @@ AddDomain() {
if [[ "${bool}" == false ]]; then
# Domain not found in the whitelist file, add it!
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} Adding $1 to $listname..."
echo -e " ${INFO} Adding ${1} to ${listname}..."
fi
reload=true
# Add it to the list we want to add it to
@ -131,28 +136,26 @@ AddDomain() {
echo -e " ${INFO} ${1} already exists in ${listname}, no need to add!"
fi
fi
elif [[ "${list}" == "${wildcardlist}" ]]; then
source "${piholeDir}/setupVars.conf"
# Remove the /* from the end of the IP addresses
IPV4_ADDRESS=${IPV4_ADDRESS%/*}
IPV6_ADDRESS=${IPV6_ADDRESS%/*}
elif [[ "${list}" == "${regexlist}" ]]; then
[[ -z "${type}" ]] && type="--wildcard-only"
bool=true
domain="${1}"
[[ "${wildcard}" == true ]] && domain="(^|\\.)${domain//\./\\.}$"
# Is the domain in the list?
grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false
# 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 $1 to wildcard blacklist..."
echo -e " ${INFO} Adding ${domain} to regex list..."
fi
reload="restart"
echo "address=/$1/${IPV4_ADDRESS}" >> "${wildcardlist}"
if [[ "${#IPV6_ADDRESS}" > 0 ]]; then
echo "address=/$1/${IPV6_ADDRESS}" >> "${wildcardlist}"
fi
echo "$domain" >> "${regexlist}"
else
if [[ "${verbose}" == true ]]; then
echo -e " ${INFO} ${1} already exists in wildcard blacklist, no need to add!"
echo -e " ${INFO} ${domain} already exists in regex list, no need to add!"
fi
fi
fi
@ -164,7 +167,6 @@ RemoveDomain() {
[[ "${list}" == "${whitelist}" ]] && listname="whitelist"
[[ "${list}" == "${blacklist}" ]] && listname="blacklist"
[[ "${list}" == "${wildcardlist}" ]] && listname="wildcard blacklist"
if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then
bool=true
@ -174,7 +176,7 @@ RemoveDomain() {
grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false
if [[ "${bool}" == true ]]; then
# Remove it from the other one
echo -e " ${INFO} Removing $1 from $listname..."
echo -e " ${INFO} Removing $1 from ${listname}..."
# /I flag: search case-insensitive
sed -i "/${domain}/Id" "${list}"
reload=true
@ -183,20 +185,25 @@ RemoveDomain() {
echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!"
fi
fi
elif [[ "${list}" == "${wildcardlist}" ]]; then
elif [[ "${list}" == "${regexlist}" ]]; then
[[ -z "${type}" ]] && type="--wildcard-only"
domain="${1}"
[[ "${wildcard}" == true ]] && domain="(^|\\.)${domain//\./\\.}$"
bool=true
# Is it in the list?
grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false
grep -Fx "${domain}" "${regexlist}" > /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}"
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} ${1} does not exist in ${listname}, no need to remove!"
echo -e " ${INFO} ${domain} does not exist in regex list, no need to remove!"
fi
fi
fi
@ -218,7 +225,7 @@ Displaylist() {
verbose=false
echo -e "Displaying $string:\n"
count=1
while IFS= read -r RD; do
while IFS= read -r RD || [ -n "${RD}" ]; do
echo " ${count}: ${RD}"
count=$((count+1))
done < "${listMain}"
@ -241,7 +248,8 @@ for var in "$@"; do
case "${var}" in
"-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";;
"-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";;
"-wild" | "wildcard" ) listMain="${wildcardlist}";;
"--wild" | "wildcard" ) listMain="${regexlist}"; wildcard=true;;
"--regex" | "regex" ) listMain="${regexlist}";;
"-nr"| "--noreload" ) reload=false;;
"-d" | "--delmode" ) addmode=false;;
"-q" | "--quiet" ) verbose=false;;

@ -17,180 +17,13 @@ source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh"
# piholeGitURL set in basic-install.sh
# is_repo() sourced from 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}"
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() {
echo " Please note that changing branches severely alters your Pi-hole subsystems"
@ -218,21 +51,21 @@ checkout() {
# This is unlikely
if ! is_repo "${PI_HOLE_FILES_DIR}" ; then
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 " ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!"
echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}"
exit 1;
fi
if [[ "${INSTALL_WEB}" == "true" ]]; then
if [[ "${INSTALL_WEB_INTERFACE}" == "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}"
echo -e " ${COL_LIGHT_RED}Error: Web Admin repo is missing from system!"
echo -e " Please re-run install script from https://github.com/pi-hole/pi-hole${COL_NC}"
exit 1;
fi
fi
if [[ -z "${1}" ]]; then
echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC}
Try 'pihole checkout --help' for more information."
echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC}"
echo -e " Try 'pihole checkout --help' for more information."
exit 1
fi
@ -246,7 +79,7 @@ checkout() {
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
if [[ "${INSTALL_WEB_INTERFACE}" == "true" ]]; then
echo ""
echo -e " ${INFO} Web interface"
fetch_checkout_pull_branch "${webInterfaceDir}" "devel" || { echo " ${CROSS} Unable to pull Web development branch"; exit 1; }
@ -257,13 +90,12 @@ checkout() {
local path
path="development/${binary}"
echo "development" > /etc/pihole/ftlbranch
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
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
@ -272,7 +104,6 @@ checkout() {
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"
@ -283,8 +114,8 @@ checkout() {
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"
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[*]}"
@ -300,7 +131,7 @@ checkout() {
exit 1
fi
checkout_pull_branch "${PI_HOLE_FILES_DIR}" "${2}"
elif [[ "${1}" == "web" ]] && [[ "${INSTALL_WEB}" == "true" ]] ; then
elif [[ "${1}" == "web" ]] && [[ "${INSTALL_WEB_INTERFACE}" == "true" ]] ; then
str="Fetching branches from ${webInterfaceGitUrl}"
echo -ne " ${INFO} $str"
if ! fully_fetch_repo "${webInterfaceDir}" ; then
@ -310,8 +141,8 @@ checkout() {
webbranches=($(get_available_branches "${webInterfaceDir}"))
if [[ "${webbranches[*]}" == *"master"* ]]; then
echo -e "${OVER} ${TICK} $str
${INFO} ${#webbranches[@]} branches available for Web Admin"
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[*]}"
@ -335,7 +166,9 @@ checkout() {
if check_download_exists "$path"; then
echo " ${TICK} Branch ${2} exists"
echo "${2}" > /etc/pihole/ftlbranch
FTLinstall "${binary}" "${path}"
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}') )
@ -350,7 +183,7 @@ checkout() {
fi
# Force updating everything
if [[ ( ! "${1}" == "web" && ! "${1}" == "ftl" ) && "${update}" == "true" ]]; then
if [[ ! "${1}" == "web" && ! "${1}" == "ftl" ]]; then
echo -e " ${INFO} Running installer to upgrade your installation"
if "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh" --unattended; then
exit 0

@ -8,6 +8,7 @@
# This file is copyright under the latest version of the EUPL.
# Please see LICENSE file for your rights under this license.
# shellcheck source=/dev/null
# -e option instructs bash to immediately exit if any command [1] has a non-zero exit status
# -u a reference to any variable you haven't previously defined
@ -37,7 +38,7 @@ else
TICK="[${COL_GREEN}${COL_NC}]"
CROSS="[${COL_RED}${COL_NC}]"
INFO="[i]"
OVER="\r\033[K"
#OVER="\r\033[K"
fi
OBFUSCATED_PLACEHOLDER="<DOMAIN OBFUSCATED>"
@ -74,7 +75,7 @@ WEB_SERVER_LOG_DIRECTORY="${LOG_DIRECTORY}/lighttpd"
WEB_SERVER_CONFIG_DIRECTORY="/etc/lighttpd"
HTML_DIRECTORY="/var/www/html"
WEB_GIT_DIRECTORY="${HTML_DIRECTORY}/admin"
BLOCK_PAGE_DIRECTORY="${HTML_DIRECTORY}/pihole"
#BLOCK_PAGE_DIRECTORY="${HTML_DIRECTORY}/pihole"
# Files required by Pi-hole
# https://discourse.pi-hole.net/t/what-files-does-pi-hole-use/1684
@ -85,14 +86,14 @@ PIHOLE_DHCP_CONFIG_FILE="${DNSMASQ_D_DIRECTORY}/02-pihole-dhcp.conf"
PIHOLE_WILDCARD_CONFIG_FILE="${DNSMASQ_D_DIRECTORY}/03-wildcard.conf"
WEB_SERVER_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/lighttpd.conf"
WEB_SERVER_CUSTOM_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/external.conf"
#WEB_SERVER_CUSTOM_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/external.conf"
PIHOLE_DEFAULT_AD_LISTS="${PIHOLE_DIRECTORY}/adlists.default"
PIHOLE_USER_DEFINED_AD_LISTS="${PIHOLE_DIRECTORY}/adlists.list"
PIHOLE_BLACKLIST_FILE="${PIHOLE_DIRECTORY}/blacklist.txt"
PIHOLE_BLOCKLIST_FILE="${PIHOLE_DIRECTORY}/gravity.list"
PIHOLE_INSTALL_LOG_FILE="${PIHOLE_DIRECTORY}/install.log"
PIHOLE_RAW_BLOCKLIST_FILES=${PIHOLE_DIRECTORY}/list.*
PIHOLE_RAW_BLOCKLIST_FILES="${PIHOLE_DIRECTORY}/list.*"
PIHOLE_LOCAL_HOSTS_FILE="${PIHOLE_DIRECTORY}/local.list"
PIHOLE_LOGROTATE_FILE="${PIHOLE_DIRECTORY}/logrotate"
PIHOLE_SETUP_VARS_FILE="${PIHOLE_DIRECTORY}/setupVars.conf"
@ -105,7 +106,7 @@ FTL_PID="${RUN_DIRECTORY}/pihole-FTL.pid"
FTL_PORT="${RUN_DIRECTORY}/pihole-FTL.port"
PIHOLE_LOG="${LOG_DIRECTORY}/pihole.log"
PIHOLE_LOG_GZIPS=${LOG_DIRECTORY}/pihole.log.[0-9].*
PIHOLE_LOG_GZIPS="${LOG_DIRECTORY}/pihole.log.[0-9].*"
PIHOLE_DEBUG_LOG="${LOG_DIRECTORY}/pihole_debug.log"
PIHOLE_DEBUG_LOG_SANITIZED="${LOG_DIRECTORY}/pihole_debug-sanitized.log"
PIHOLE_FTL_LOG="${LOG_DIRECTORY}/pihole-FTL.log"
@ -115,53 +116,52 @@ PIHOLE_WEB_SERVER_ERROR_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/error.log"
# An array of operating system "pretty names" that we officialy support
# We can loop through the array at any time to see if it matches a value
SUPPORTED_OS=("Raspbian" "Ubuntu" "Fedora" "Debian" "CentOS")
#SUPPORTED_OS=("Raspbian" "Ubuntu" "Fedora" "Debian" "CentOS")
# Store Pi-hole's processes in an array for easy use and parsing
PIHOLE_PROCESSES=( "dnsmasq" "lighttpd" "pihole-FTL" )
# Store the required directories in an array so it can be parsed through
REQUIRED_DIRECTORIES=(${CORE_GIT_DIRECTORY}
${CRON_D_DIRECTORY}
${DNSMASQ_D_DIRECTORY}
${PIHOLE_DIRECTORY}
${PIHOLE_SCRIPTS_DIRECTORY}
${BIN_DIRECTORY}
${RUN_DIRECTORY}
${LOG_DIRECTORY}
${WEB_SERVER_LOG_DIRECTORY}
${WEB_SERVER_CONFIG_DIRECTORY}
${HTML_DIRECTORY}
${WEB_GIT_DIRECTORY}
${BLOCK_PAGE_DIRECTORY})
#REQUIRED_DIRECTORIES=("${CORE_GIT_DIRECTORY}"
#"${CRON_D_DIRECTORY}"
#"${DNSMASQ_D_DIRECTORY}"
#"${PIHOLE_DIRECTORY}"
#"${PIHOLE_SCRIPTS_DIRECTORY}"
#"${BIN_DIRECTORY}"
#"${RUN_DIRECTORY}"
#"${LOG_DIRECTORY}"
#"${WEB_SERVER_LOG_DIRECTORY}"
#"${WEB_SERVER_CONFIG_DIRECTORY}"
#"${HTML_DIRECTORY}"
#"${WEB_GIT_DIRECTORY}"
#"${BLOCK_PAGE_DIRECTORY}")
# Store the required directories in an array so it can be parsed through
mapfile -t array <<< "$var"
REQUIRED_FILES=(${PIHOLE_CRON_FILE}
${PIHOLE_DNS_CONFIG_FILE}
${PIHOLE_DHCP_CONFIG_FILE}
${PIHOLE_WILDCARD_CONFIG_FILE}
${WEB_SERVER_CONFIG_FILE}
${PIHOLE_DEFAULT_AD_LISTS}
${PIHOLE_USER_DEFINED_AD_LISTS}
${PIHOLE_BLACKLIST_FILE}
${PIHOLE_BLOCKLIST_FILE}
${PIHOLE_INSTALL_LOG_FILE}
${PIHOLE_RAW_BLOCKLIST_FILES}
${PIHOLE_LOCAL_HOSTS_FILE}
${PIHOLE_LOGROTATE_FILE}
${PIHOLE_SETUP_VARS_FILE}
${PIHOLE_WHITELIST_FILE}
${PIHOLE_COMMAND}
${PIHOLE_COLTABLE_FILE}
${FTL_PID}
${FTL_PORT}
${PIHOLE_LOG}
${PIHOLE_LOG_GZIPS}
${PIHOLE_DEBUG_LOG}
${PIHOLE_FTL_LOG}
${PIHOLE_WEB_SERVER_ACCESS_LOG_FILE}
${PIHOLE_WEB_SERVER_ERROR_LOG_FILE})
REQUIRED_FILES=("${PIHOLE_CRON_FILE}"
"${PIHOLE_DNS_CONFIG_FILE}"
"${PIHOLE_DHCP_CONFIG_FILE}"
"${PIHOLE_WILDCARD_CONFIG_FILE}"
"${WEB_SERVER_CONFIG_FILE}"
"${PIHOLE_DEFAULT_AD_LISTS}"
"${PIHOLE_USER_DEFINED_AD_LISTS}"
"${PIHOLE_BLACKLIST_FILE}"
"${PIHOLE_BLOCKLIST_FILE}"
"${PIHOLE_INSTALL_LOG_FILE}"
"${PIHOLE_RAW_BLOCKLIST_FILES}"
"${PIHOLE_LOCAL_HOSTS_FILE}"
"${PIHOLE_LOGROTATE_FILE}"
"${PIHOLE_SETUP_VARS_FILE}"
"${PIHOLE_WHITELIST_FILE}"
"${PIHOLE_COMMAND}"
"${PIHOLE_COLTABLE_FILE}"
"${FTL_PID}"
"${FTL_PORT}"
"${PIHOLE_LOG}"
"${PIHOLE_LOG_GZIPS}"
"${PIHOLE_DEBUG_LOG}"
"${PIHOLE_FTL_LOG}"
"${PIHOLE_WEB_SERVER_ACCESS_LOG_FILE}"
"${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}")
DISCLAIMER="This process collects information from your Pi-hole, and optionally uploads it to a unique and random directory on tricorder.pi-hole.net.
@ -176,7 +176,7 @@ show_disclaimer(){
source_setup_variables() {
# Display the current test that is running
log_write "\n${COL_PURPLE}*** [ INITIALIZING ]${COL_NC} Sourcing setup variables"
log_write "\\n${COL_PURPLE}*** [ INITIALIZING ]${COL_NC} Sourcing setup variables"
# If the variable file exists,
if ls "${PIHOLE_SETUP_VARS_FILE}" 1> /dev/null 2>&1; then
log_write "${INFO} Sourcing ${PIHOLE_SETUP_VARS_FILE}...";
@ -231,7 +231,7 @@ initialize_debug() {
echo_current_diagnostic() {
# Colors are used for visually distinguishing each test in the output
# These colors do not show in the GUI, but the formatting will
log_write "\n${COL_PURPLE}*** [ DIAGNOSING ]:${COL_NC} ${1}"
log_write "\\n${COL_PURPLE}*** [ DIAGNOSING ]:${COL_NC} ${1}"
}
compare_local_version_to_git_version() {
@ -245,6 +245,7 @@ compare_local_version_to_git_version() {
local search_term="Pi-hole"
elif [[ "${pihole_component}" == "Web" ]]; then
# We need to search for "AdminLTE" so store it in a variable as well
#shellcheck disable=2034
local search_term="AdminLTE"
fi
# Display what we are checking
@ -365,7 +366,8 @@ check_critical_program_versions() {
is_os_supported() {
local os_to_check="${1}"
# Strip just the base name of the system using sed
the_os=$(echo ${os_to_check} | sed 's/ .*//')
# shellcheck disable=SC2001
the_os=$(echo "${os_to_check}" | sed 's/ .*//')
# If the variable is one of our supported OSes,
case "${the_os}" in
# Print it in green
@ -384,6 +386,8 @@ get_distro_attributes() {
OLD_IFS="$IFS"
# Store the distro info in an array and make it global since the OS won't change,
# but we'll keep it within the function for better unit testing
local distro_info
#shellcheck disable=SC2016
IFS=$'\r\n' command eval 'distro_info=( $(cat /etc/*release) )'
# Set a named variable for better readability
@ -391,7 +395,8 @@ get_distro_attributes() {
# For each line found in an /etc/*release file,
for distro_attribute in "${distro_info[@]}"; do
# store the key in a variable
local pretty_name_key=$(echo "${distro_attribute}" | grep "PRETTY_NAME" | cut -d '=' -f1)
local pretty_name_key
pretty_name_key=$(echo "${distro_attribute}" | grep "PRETTY_NAME" | cut -d '=' -f1)
# we need just the OS PRETTY_NAME,
if [[ "${pretty_name_key}" == "PRETTY_NAME" ]]; then
# so save in in a variable when we find it
@ -465,15 +470,15 @@ processor_check() {
else
# Check if the architecture is currently supported for FTL
case "${PROCESSOR}" in
"amd64") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
"amd64") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
;;
"armv6l") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
"armv6l") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
;;
"armv6") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
"armv6") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
;;
"armv7l") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
"armv7l") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
;;
"aarch64") "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
"aarch64") log_write "${TICK} ${COL_GREEN}${PROCESSOR}${COL_NC}"
;;
# Otherwise, show the processor type
*) log_write "${INFO} ${PROCESSOR}";
@ -493,13 +498,21 @@ parse_setup_vars() {
fi
}
parse_locale() {
local pihole_locale
echo_current_diagnostic "Locale"
pihole_locale="$(locale)"
parse_file "${pihole_locale}"
}
does_ip_match_setup_vars() {
# Check for IPv4 or 6
local protocol="${1}"
# IP address to check for
local ip_address="${2}"
# See what IP is in the setupVars.conf file
local setup_vars_ip=$(< ${PIHOLE_SETUP_VARS_FILE} grep IPV${protocol}_ADDRESS | cut -d '=' -f2)
local setup_vars_ip
setup_vars_ip=$(< ${PIHOLE_SETUP_VARS_FILE} grep IPV"${protocol}"_ADDRESS | cut -d '=' -f2)
# If it's an IPv6 address
if [[ "${protocol}" == "6" ]]; then
# Strip off the / (CIDR notation)
@ -530,10 +543,10 @@ detect_ip_addresses() {
# Use ip to show the addresses for the chosen protocol
# Store the values in an arry so they can be looped through
# Get the lines that are in the file(s) and store them in an array for parsing later
declare -a ip_addr_list=( $(ip -${protocol} addr show dev ${PIHOLE_INTERFACE} | awk -F ' ' '{ for(i=1;i<=NF;i++) if ($i ~ '/^inet/') print $(i+1) }') )
mapfile -t ip_addr_list < <(ip -"${protocol}" addr show dev "${PIHOLE_INTERFACE}" | awk -F ' ' '{ for(i=1;i<=NF;i++) if ($i ~ '/^inet/') print $(i+1) }')
# If there is something in the IP address list,
if [[ -n ${ip_addr_list} ]]; then
if [[ -n ${ip_addr_list[*]} ]]; then
# Local iterator
local i
# Display the protocol and interface
@ -547,15 +560,15 @@ detect_ip_addresses() {
log_write ""
else
# If there are no IPs detected, explain that the protocol is not configured
log_write "${CROSS} ${COL_RED}No IPv${protocol} address(es) found on the ${PIHOLE_INTERFACE}${COL_NC} interface.\n"
log_write "${CROSS} ${COL_RED}No IPv${protocol} address(es) found on the ${PIHOLE_INTERFACE}${COL_NC} interface.\\n"
return 1
fi
# If the protocol is v6
if [[ "${protocol}" == "6" ]]; then
# let the user know that as long as there is one green address, things should be ok
log_write " ^ Please note that you may have more than one IP address listed."
log_write " As long as one of them is green, and it matches what is in ${PIHOLE_SETUP_VARS_FILE}, there is no need for concern.\n"
log_write " The link to the FAQ is for an issue that sometimes occurs when the IPv6 address changes, which is why we check for it.\n"
log_write " As long as one of them is green, and it matches what is in ${PIHOLE_SETUP_VARS_FILE}, there is no need for concern.\\n"
log_write " The link to the FAQ is for an issue that sometimes occurs when the IPv6 address changes, which is why we check for it.\\n"
fi
}
@ -582,7 +595,7 @@ ping_gateway() {
# Check if we are using IPv4 or IPv6
# Find the default gateway using IPv4 or IPv6
local gateway
gateway="$(ip -${protocol} route | grep default | cut -d ' ' -f 3)"
gateway="$(ip -"${protocol}" route | grep default | cut -d ' ' -f 3)"
# If the gateway variable has a value (meaning a gateway was found),
if [[ -n "${gateway}" ]]; then
@ -592,9 +605,9 @@ ping_gateway() {
# Try to quietly ping the gateway 3 times, with a timeout of 3 seconds, using numeric output only,
# on the pihole interface, and tail the last three lines of the output
# If pinging the gateway is not successful,
if ! ${cmd} -c 3 -W 2 -n ${gateway} -I ${PIHOLE_INTERFACE} >/dev/null; then
if ! ${cmd} -c 1 -W 2 -n "${gateway}" -I "${PIHOLE_INTERFACE}" >/dev/null; then
# let the user know
log_write "${CROSS} ${COL_RED}Gateway did not respond.${COL_NC} ($FAQ_GATEWAY)\n"
log_write "${CROSS} ${COL_RED}Gateway did not respond.${COL_NC} ($FAQ_GATEWAY)\\n"
# and return an error code
return 1
# Otherwise,
@ -613,13 +626,13 @@ ping_internet() {
ping_ipv4_or_ipv6 "${protocol}"
log_write "* Checking Internet connectivity via IPv${protocol}..."
# Try to ping the address 3 times
if ! ${cmd} -W 2 -c 3 -n ${public_address} -I ${PIHOLE_INTERFACE} >/dev/null; then
if ! ${cmd} -c 1 -W 2 -n ${public_address} -I "${PIHOLE_INTERFACE}" >/dev/null; then
# if it's unsuccessful, show an error
log_write "${CROSS} ${COL_RED}Cannot reach the Internet.${COL_NC}\n"
log_write "${CROSS} ${COL_RED}Cannot reach the Internet.${COL_NC}\\n"
return 1
else
# Otherwise, show success
log_write "${TICK} ${COL_GREEN}Query responded.${COL_NC}\n"
log_write "${TICK} ${COL_GREEN}Query responded.${COL_NC}\\n"
return 0
fi
}
@ -652,15 +665,22 @@ check_required_ports() {
# Sort the addresses and remove duplicates
while IFS= read -r line; do
ports_in_use+=( "$line" )
done < <( lsof -i -P -n | awk -F' ' '/LISTEN/ {print $9, $1}' | sort -n | uniq | cut -d':' -f2 )
done < <( lsof -iTCP -sTCP:LISTEN -P -n +c 10 )
# Now that we have the values stored,
for i in "${!ports_in_use[@]}"; do
# loop through them and assign some local variables
local port_number
port_number="$(echo "${ports_in_use[$i]}" | awk '{print $1}')"
local service_name
service_name=$(echo "${ports_in_use[$i]}" | awk '{print $2}')
service_name=$(echo "${ports_in_use[$i]}" | awk '{print $1}')
local protocol_type
protocol_type=$(echo "${ports_in_use[$i]}" | awk '{print $5}')
local port_number
port_number="$(echo "${ports_in_use[$i]}" | awk '{print $9}')"
# Skip the line if it's the titles of the columns the lsof command produces
if [[ "${service_name}" == COMMAND ]]; then
continue
fi
# Use a case statement to determine if the right services are using the right ports
case "${port_number}" in
53) compare_port_to_service_assigned "${resolver}"
@ -670,7 +690,7 @@ check_required_ports() {
4711) compare_port_to_service_assigned "${ftl}"
;;
# If it's not a default port that Pi-hole needs, just print it out for the user to see
*) log_write "[${port_number}] is in use by ${service_name}";
*) log_write "${port_number} ${service_name} (${protocol_type})";
esac
done
}
@ -712,20 +732,20 @@ check_x_headers() {
# If the X-header found by curl matches what is should be,
if [[ $block_page == "$block_page_working" ]]; then
# display a success message
log_write "$TICK ${COL_GREEN}${block_page}${COL_NC}"
log_write "$TICK Block page X-Header: ${COL_GREEN}${block_page}${COL_NC}"
else
# Otherwise, show an error
log_write "$CROSS ${COL_RED}X-Header does not match or could not be retrieved.${COL_NC}"
log_write "$CROSS Block page X-Header: ${COL_RED}X-Header does not match or could not be retrieved.${COL_NC}"
log_write "${COL_RED}${full_curl_output_block_page}${COL_NC}"
fi
# Same logic applies to the dashbord as above, if the X-Header matches what a working system shoud have,
if [[ $dashboard == "$dashboard_working" ]]; then
# then we can show a success
log_write "$TICK ${COL_GREEN}${dashboard}${COL_NC}"
log_write "$TICK Web interface X-Header: ${COL_GREEN}${dashboard}${COL_NC}"
else
# Othewise, it's a failure since the X-Headers either don't exist or have been modified in some way
log_write "$CROSS ${COL_RED}X-Header does not match or could not be retrieved.${COL_NC}"
log_write "$CROSS Web interface X-Header: ${COL_RED}X-Header does not match or could not be retrieved.${COL_NC}"
log_write "${COL_RED}${full_curl_output_dashboard}${COL_NC}"
fi
}
@ -751,14 +771,14 @@ dig_at() {
if [[ ${protocol} == "6" ]]; then
# Set the IPv6 variables and record type
local local_address="::1"
local pihole_address="${IPV6_ADDRESS%/*}"
local pihole_address="${IP}"
local remote_address="2001:4860:4860::8888"
local record_type="AAAA"
# Othwerwise, it should be 4
else
# so use the IPv4 values
local local_address="127.0.0.1"
local pihole_address="${IPV4_ADDRESS%/*}"
local pihole_address="${IP}"
local remote_address="8.8.8.8"
local record_type="A"
fi
@ -766,7 +786,8 @@ dig_at() {
# Find a random blocked url that has not been whitelisted.
# This helps emulate queries to different domains that a user might query
# It will also give extra assurance that Pi-hole is correctly resolving and blocking domains
local random_url=$(shuf -n 1 "${PIHOLE_BLOCKLIST_FILE}" | awk -F ' ' '{ print $2 }')
local random_url
random_url=$(shuf -n 1 "${PIHOLE_BLOCKLIST_FILE}")
# First, do a dig on localhost to see if Pi-hole can use itself to block a domain
if local_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @${local_address} +short "${record_type}"); then
@ -783,7 +804,7 @@ dig_at() {
# The default timeouts and tries are reduced in case the DNS server isn't working, so the user isn't waiting for too long
# If Pi-hole can dig itself from it's IP (not the loopback address)
if pihole_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @${pihole_address} +short "${record_type}"); then
if pihole_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @"${pihole_address}" +short "${record_type}"); then
# show a success
log_write "${TICK} ${random_url} ${COL_GREEN}is ${pihole_dig}${COL_NC} via ${COL_CYAN}Pi-hole${COL_NC} (${pihole_address})"
else
@ -812,10 +833,12 @@ process_status(){
# If systemd
if command -v systemctl &> /dev/null; then
# get its status via systemctl
local status_of_process=$(systemctl is-active "${i}")
local status_of_process
status_of_process=$(systemctl is-active "${i}")
else
# Otherwise, use the service command
local status_of_process=$(service "${i}" status | awk '/Active:/ {print $2}') &> /dev/null
local status_of_process
status_of_process=$(service "${i}" status | awk '/Active:/ {print $2}') &> /dev/null
fi
# and print it out to the user
if [[ "${status_of_process}" == "active" ]]; then
@ -879,15 +902,20 @@ parse_file() {
# Put the current Internal Field Separator into another variable so it can be restored later
OLD_IFS="$IFS"
# Get the lines that are in the file(s) and store them in an array for parsing later
local file_info
if [[ -f "$filename" ]]; then
#shellcheck disable=SC2016
IFS=$'\r\n' command eval 'file_info=( $(cat "${filename}") )'
else
read -a file_info <<< $filename
fi
# Set a named variable for better readability
local file_lines
# For each line in the file,
for file_lines in "${file_info[@]}"; do
if [[ ! -z "${file_lines}" ]]; then
# don't include the Web password hash
[[ "${file_linesline}" =~ ^\#.*$ || ! "${file_lines}" || "${file_lines}" == "WEBPASSWORD="* ]] && continue
[[ "${file_lines}" =~ ^\#.*$ || ! "${file_lines}" || "${file_lines}" == "WEBPASSWORD="* ]] && continue
# otherwise, display the lines of the file
log_write " ${file_lines}"
fi
@ -931,7 +959,7 @@ list_files_in_dir() {
# Set the first argument passed to tihs function as a named variable for better readability
local dir_to_parse="${1}"
# Store the files found in an array
local files_found=( $(ls "${dir_to_parse}") )
mapfile -t files_found < <(ls "${dir_to_parse}")
# For each file in the array,
for each_file in "${files_found[@]}"; do
if [[ -d "${dir_to_parse}/${each_file}" ]]; then
@ -939,26 +967,26 @@ list_files_in_dir() {
:
elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_BLOCKLIST_FILE}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \
[[ ${dir_to_parse}/${each_file} == ${PIHOLE_RAW_BLOCKLIST_FILES} ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_SETUP_VARS_FILE}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG}" ]] || \
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_WEB_SERVER_ACCESS_LOG_FILE}" ]] || \
[[ ${dir_to_parse}/${each_file} == ${PIHOLE_LOG_GZIPS} ]]; then
[[ "${dir_to_parse}/${each_file}" == "${PIHOLE_LOG_GZIPS}" ]]; then
:
else
# Then, parse the file's content into an array so each line can be analyzed if need be
for i in "${!REQUIRED_FILES[@]}"; do
if [[ "${dir_to_parse}/${each_file}" == ${REQUIRED_FILES[$i]} ]]; then
if [[ "${dir_to_parse}/${each_file}" == "${REQUIRED_FILES[$i]}" ]]; then
# display the filename
log_write "\n${COL_GREEN}$(ls -ld ${dir_to_parse}/${each_file})${COL_NC}"
log_write "\\n${COL_GREEN}$(ls -ld "${dir_to_parse}"/"${each_file}")${COL_NC}"
# Check if the file we want to view has a limit (because sometimes we just need a little bit of info from the file, not the entire thing)
case "${dir_to_parse}/${each_file}" in
# If it's Web server error log, just give the first 25 lines
"${PIHOLE_WEB_SERVER_ERROR_LOG_FILE}") make_array_from_file "${dir_to_parse}/${each_file}" 25
;;
# Same for the FTL log
"${PIHOLE_FTL_LOG}") make_array_from_file "${dir_to_parse}/${each_file}" 25
"${PIHOLE_FTL_LOG}") head_tail_log "${dir_to_parse}/${each_file}" 35
;;
# parse the file into an array in case we ever need to analyze it line-by-line
*) make_array_from_file "${dir_to_parse}/${each_file}";
@ -991,6 +1019,34 @@ show_content_of_pihole_files() {
show_content_of_files_in_dir "${LOG_DIRECTORY}"
}
head_tail_log() {
# The file being processed
local filename="${1}"
# The number of lines to use for head and tail
local qty="${2}"
local head_line
local tail_line
# Put the current Internal Field Separator into another variable so it can be restored later
OLD_IFS="$IFS"
# Get the lines that are in the file(s) and store them in an array for parsing later
IFS=$'\r\n'
local log_head=()
mapfile -t log_head < <(head -n "${qty}" "${filename}")
log_write " ${COL_CYAN}-----head of $(basename "${filename}")------${COL_NC}"
for head_line in "${log_head[@]}"; do
log_write " ${head_line}"
done
log_write ""
local log_tail=()
mapfile -t log_tail < <(tail -n "${qty}" "${filename}")
log_write " ${COL_CYAN}-----tail of $(basename "${filename}")------${COL_NC}"
for tail_line in "${log_tail[@]}"; do
log_write " ${tail_line}"
done
# Set the IFS back to what it was
IFS="$OLD_IFS"
}
analyze_gravity_list() {
echo_current_diagnostic "Gravity list"
local head_line
@ -999,17 +1055,18 @@ analyze_gravity_list() {
OLD_IFS="$IFS"
# Get the lines that are in the file(s) and store them in an array for parsing later
IFS=$'\r\n'
local gravity_permissions=$(ls -ld "${PIHOLE_BLOCKLIST_FILE}")
local gravity_permissions
gravity_permissions=$(ls -ld "${PIHOLE_BLOCKLIST_FILE}")
log_write "${COL_GREEN}${gravity_permissions}${COL_NC}"
local gravity_head=()
gravity_head=( $(head -n 4 ${PIHOLE_BLOCKLIST_FILE}) )
mapfile -t gravity_head < <(head -n 4 ${PIHOLE_BLOCKLIST_FILE})
log_write " ${COL_CYAN}-----head of $(basename ${PIHOLE_BLOCKLIST_FILE})------${COL_NC}"
for head_line in "${gravity_head[@]}"; do
log_write " ${head_line}"
done
log_write ""
local gravity_tail=()
gravity_tail=( $(tail -n 4 ${PIHOLE_BLOCKLIST_FILE}) )
mapfile -t gravity_tail < <(tail -n 4 ${PIHOLE_BLOCKLIST_FILE})
log_write " ${COL_CYAN}-----tail of $(basename ${PIHOLE_BLOCKLIST_FILE})------${COL_NC}"
for tail_line in "${gravity_tail[@]}"; do
log_write " ${tail_line}"
@ -1025,10 +1082,11 @@ analyze_pihole_log() {
OLD_IFS="$IFS"
# Get the lines that are in the file(s) and store them in an array for parsing later
IFS=$'\r\n'
local pihole_log_permissions=$(ls -ld "${PIHOLE_LOG}")
local pihole_log_permissions
pihole_log_permissions=$(ls -ld "${PIHOLE_LOG}")
log_write "${COL_GREEN}${pihole_log_permissions}${COL_NC}"
local pihole_log_head=()
pihole_log_head=( $(head -n 20 ${PIHOLE_LOG}) )
mapfile -t pihole_log_head < <(head -n 20 ${PIHOLE_LOG})
log_write " ${COL_CYAN}-----head of $(basename ${PIHOLE_LOG})------${COL_NC}"
local error_to_check_for
local line_to_obfuscate
@ -1038,10 +1096,10 @@ analyze_pihole_log() {
# that the DNS server is attempting to read. Since it's not formatted
# correctly, there will be an entry for "bad address at line n"
# So we can check for that here and highlight it in red so the user can see it easily
error_to_check_for=$(echo ${head_line} | grep 'bad address at')
error_to_check_for=$(echo "${head_line}" | grep 'bad address at')
# Some users may not want to have the domains they visit sent to us
# To that end, we check for lines in the log that would contain a domain name
line_to_obfuscate=$(echo ${head_line} | grep ': query\|: forwarded\|: reply')
line_to_obfuscate=$(echo "${head_line}" | grep ': query\|: forwarded\|: reply')
# If the variable contains a value, it found an error in the log
if [[ -n ${error_to_check_for} ]]; then
# So we can print it in red to make it visible to the user
@ -1056,7 +1114,7 @@ analyze_pihole_log() {
if [[ -n ${line_to_obfuscate} ]]; then
# If there are, we need to use awk to replace only the domain name (the 6th field in the log)
# so we substitue the domain for the placeholder value
obfuscated_line=$(echo ${line_to_obfuscate} | awk -v placeholder="${OBFUSCATED_PLACEHOLDER}" '{sub($6,placeholder); print $0}')
obfuscated_line=$(echo "${line_to_obfuscate}" | awk -v placeholder="${OBFUSCATED_PLACEHOLDER}" '{sub($6,placeholder); print $0}')
log_write " ${obfuscated_line}"
else
log_write " ${head_line}"
@ -1097,7 +1155,7 @@ upload_to_tricorder() {
log_write ""
log_write "${COL_PURPLE}********************************************${COL_NC}"
log_write "${COL_PURPLE}********************************************${COL_NC}"
log_write "${TICK} ${COL_GREEN}** FINISHED DEBUGGING! **${COL_NC}\n"
log_write "${TICK} ${COL_GREEN}** FINISHED DEBUGGING! **${COL_NC}\\n"
# Provide information on what they should do with their token
log_write " * The debug log can be uploaded to tricorder.pi-hole.net for sharing with developers only."
@ -1144,7 +1202,7 @@ upload_to_tricorder() {
log_write " * Please try again or contact the Pi-hole team for assistance."
fi
# Finally, show where the log file is no matter the outcome of the function so users can look at it
log_write " * A local copy of the debug log can be found at: ${COL_CYAN}${PIHOLE_DEBUG_LOG_SANITIZED}${COL_NC}\n"
log_write " * A local copy of the debug log can be found at: ${COL_CYAN}${PIHOLE_DEBUG_LOG_SANITIZED}${COL_NC}\\n"
}
# Run through all the functions we made
@ -1165,6 +1223,7 @@ parse_setup_vars
check_x_headers
analyze_gravity_list
show_content_of_pihole_files
parse_locale
analyze_pihole_log
copy_to_debug_log
upload_to_tricorder

@ -16,7 +16,10 @@ source ${colfile}
# Constructed to return nothing when
# a) the setting is not present in the config file, or
# 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.
if [ -z "$DBFILE" ]; then
DBFILE="/etc/pihole/pihole-FTL.db"

@ -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

@ -19,6 +19,9 @@ readonly PI_HOLE_FILES_DIR="/etc/.pihole"
# shellcheck disable=SC2034
PH_TEST=true
# when --check-only is passed to this script, it will not perform the actual update
CHECK_ONLY=false
# shellcheck disable=SC1090
source "${PI_HOLE_FILES_DIR}/automated install/basic-install.sh"
# shellcheck disable=SC1091
@ -28,9 +31,12 @@ source "/opt/pihole/COL_TABLE"
# make_repo() sourced from basic-install.sh
# update_repo() source from basic-install.sh
# getGitFiles() sourced from basic-install.sh
# get_binary_name() sourced from basic-install.sh
# FTLcheckUpdate() sourced from basic-install.sh
GitCheckUpdateAvail() {
local directory="${1}"
local directory
directory="${1}"
curdir=$PWD
cd "${directory}" || return
@ -51,14 +57,14 @@ GitCheckUpdateAvail() {
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
Additional debugging output:${COL_NC}"
echo -e "\\n ${COL_LIGHT_RED}Error: Local revision could not be obtained, please contact Pi-hole Support"
echo -e " Additional debugging output:${COL_NC}"
git status
exit
fi
if [[ "${#REMOTE}" == 0 ]]; then
echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support
Additional debugging output:${COL_NC}"
echo -e "\\n ${COL_LIGHT_RED}Error: Remote revision could not be obtained, please contact Pi-hole Support"
echo -e " Additional debugging output:${COL_NC}"
git status
exit
fi
@ -77,31 +83,23 @@ GitCheckUpdateAvail() {
fi
}
FTLcheckUpdate() {
local FTLversion
FTLversion=$(/usr/bin/pihole-FTL tag)
local FTLlatesttag
FTLlatesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep 'Location' | awk -F '/' '{print $NF}' | tr -d '\r\n')
if [[ "${FTLversion}" != "${FTLlatesttag}" ]]; then
return 0
else
return 1
fi
}
main() {
local pihole_version_current
local web_version_current
local basicError="\\n ${COL_LIGHT_RED}Unable to complete update, please contact Pi-hole Support${COL_NC}"
local core_update
local web_update
local FTL_update
core_update=false
web_update=false
FTL_update=false
# 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}"
echo -e "\\n ${COL_LIGHT_RED}Error: Core Pi-hole repo is missing from system!"
echo -e " Please re-run install script from https://pi-hole.net${COL_NC}"
exit 1;
fi
@ -115,28 +113,10 @@ main() {
echo -e " ${INFO} Pi-hole Core:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
if FTLcheckUpdate ; then
FTL_update=true
echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}"
else
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
# since the core update will run the installer which will itself
# re-install (i.e. update) FTL
if ${FTL_update} && ! ${core_update}; then
echo ""
echo -e " ${INFO} FTL out of date"
FTLdetect
echo ""
fi
if [[ "${INSTALL_WEB}" == true ]]; then
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!
Please re-run install script from https://pi-hole.net${COL_NC}"
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
@ -147,82 +127,65 @@ main() {
web_update=false
echo -e " ${INFO} Web Interface:\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
fi
fi
# Logic
# If Core up to date AND web up to date:
# Do nothing
# If Core up to date AND web NOT up to date:
# Pull web repo
# If Core NOT up to date AND web up to date:
# pull pihole repo, run install --unattended -- reconfigure
# if Core NOT up to date AND web NOT up to date:
# pull pihole repo run install --unattended
if FTLcheckUpdate > /dev/null; then
FTL_update=true
echo -e " ${INFO} FTL:\\t\\t${COL_YELLOW}update available${COL_NC}"
else
case $? in
1)
echo -e " ${INFO} FTL:\\t\\t${COL_LIGHT_GREEN}up to date${COL_NC}"
;;
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."
;;
*)
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} && ! ${web_update} ; then
if ! ${FTL_update} ; then
if [[ "${core_update}" == false && "${web_update}" == false && "${FTL_update}" == false ]]; then
echo ""
echo -e " ${TICK} Everything is up to date!"
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
else # Web Admin not installed, so only verify if core is up to date
if ! ${core_update}; then
if ! ${FTL_update} ; then
if [[ "${CHECK_ONLY}" == true ]]; then
echo ""
echo -e " ${INFO} Everything is up to date!"
exit 0
fi
else
if [[ "${core_update}" == true ]]; then
echo ""
echo -e " ${INFO} Pi-hole Core files out of date"
echo -e " ${INFO} Pi-hole core files out of date, updating local repo."
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
echo -e " ${INFO} If you had made any changes in '/etc/.pihole/', they have been stashed using 'git stash'"
fi
if [[ "${web_update}" == true ]]; then
web_version_current="$(/usr/local/bin/pihole version --admin --current)"
echo ""
echo -e " ${INFO} Web Admin version is now at ${web_version_current/* v/v}
${INFO} If you had made any changes in '/var/www/html/admin/', they have been stashed using 'git stash'"
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 [[ "${core_update}" == true ]]; then
pihole_version_current="$(/usr/local/bin/pihole version --pihole --current)"
if [[ "${FTL_update}" == true ]]; then
echo ""
echo -e " ${INFO} Pi-hole version is now at ${pihole_version_current/* v/v}
${INFO} If you had made any changes in '/etc/.pihole/', they have been stashed using 'git stash'"
echo -e " ${INFO} FTL out of date, it will be updated by the installer."
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
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 ""
exit 0
}
if [[ "$1" == "--check-only" ]]; then
CHECK_ONLY=true
fi
main

@ -13,6 +13,7 @@
readonly setupVars="/etc/pihole/setupVars.conf"
readonly dnsmasqconfig="/etc/dnsmasq.d/01-pihole.conf"
readonly dhcpconfig="/etc/dnsmasq.d/02-pihole-dhcp.conf"
readonly FTLconf="/etc/pihole/pihole-FTL.conf"
# 03 -> wildcards
readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf"
@ -35,7 +36,7 @@ Options:
-e, email Set an administrative contact address for the Block Page
-h, --help Show this help dialog
-i, interface Specify dnsmasq's interface listening behavior
Add '-h' for more info on interface usage"
-l, privacylevel Set privacy level (0 = lowest, 3 = highest)"
exit 0
}
@ -52,6 +53,19 @@ change_setting() {
add_setting "${1}" "${2}"
}
addFTLsetting() {
echo "${1}=${2}" >> "${FTLconf}"
}
deleteFTLsetting() {
sed -i "/${1}/d" "${FTLconf}"
}
changeFTLsetting() {
deleteFTLsetting "${1}"
addFTLsetting "${1}" "${2}"
}
add_dnsmasq_setting() {
if [[ "${2}" != "" ]]; then
echo "${1}=${2}" >> "${dnsmasqconfig}"
@ -135,6 +149,14 @@ ProcessDNSSettings() {
let COUNTER=COUNTER+1
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"
if [[ "${DNS_FQDN_REQUIRED}" == true ]]; then
@ -182,11 +204,11 @@ trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC68345710423
add_dnsmasq_setting "interface" "${PIHOLE_INTERFACE}"
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() {
@ -215,6 +237,7 @@ SetDNSServers() {
else
change_setting "DNSSEC" "false"
fi
if [[ "${args[6]}" == "conditional_forwarding" ]]; then
change_setting "CONDITIONAL_FORWARDING" "true"
change_setting "CONDITIONAL_FORWARDING_IP" "${args[7]}"
@ -361,7 +384,9 @@ CustomizeAdLists() {
elif [[ "${args[2]}" == "disable" ]]; then
sed -i "\\@${args[3]}@s/^http/#http/g" "${list}"
elif [[ "${args[2]}" == "add" ]]; then
if [[ $(grep -c "^${args[3]}$" "${list}") -eq 0 ]] ; then
echo "${args[3]}" >> ${list}
fi
elif [[ "${args[2]}" == "del" ]]; then
var=$(echo "${args[3]}" | sed 's/\//\\\//g')
sed -i "/${var}/Id" "${list}"
@ -505,6 +530,13 @@ audit()
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() {
args=("$@")
@ -534,6 +566,7 @@ main() {
"-t" | "teleporter" ) Teleporter;;
"adlist" ) CustomizeAdLists;;
"audit" ) audit;;
"-l" | "privacylevel" ) SetPrivacyLevel;;
* ) helpFunc;;
esac

@ -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
}
# Start the service
start() {
if is_running; then
@ -29,9 +30,12 @@ start() {
mkdir -p /var/run/pihole
mkdir -p /var/log/pihole
chown pihole:pihole /var/run/pihole /var/log/pihole
rm /var/run/pihole/FTL.sock
chown pihole:pihole /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /etc/pihole
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
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
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"
echo
fi
@ -40,6 +44,7 @@ start() {
# Stop the service
stop() {
if is_running; then
/sbin/resolvconf -d lo.piholeFTL
kill "$(get_pid)"
for i in {1..5}; do
if ! is_running; then
@ -64,13 +69,25 @@ stop() {
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 ###
case "$1" in
stop)
stop
;;
status)
status pihole-FTL
status
;;
start|restart|reload|condrestart)
stop

@ -1,11 +1,79 @@
_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=()
cur="${COMP_WORDS[COMP_CWORD]}"
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]}"
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
}
complete -F _pihole pihole

@ -64,7 +64,7 @@ if ($serverName === "pi.hole") {
<html><head>
$viewPort
<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
@ -102,8 +102,10 @@ if ($serverName === "pi.hole") {
$bpAskAdmin = !empty($svEmail) ? '<a href="mailto:'.$svEmail.'?subject=Site Blocked: '.$serverName.'"></a>' : "<span/>";
// 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>.");
}
// Set location of adlists file
if (is_file("/etc/pihole/adlists.list")) {
@ -327,6 +329,7 @@ setHeader();
setTimeout(function(){window.location.reload(1);}, 10000);
$("#bpOutput").removeClass("add");
$("#bpOutput").addClass("success");
$("#bpOutput").html("");
} else {
$("#bpOutput").removeClass("add");
$("#bpOutput").addClass("error");
@ -336,6 +339,7 @@ setHeader();
error: function(jqXHR, exception) {
$("#bpOutput").removeClass("add");
$("#bpOutput").addClass("exception");
$("#bpOutput").html("");
}
});
}

File diff suppressed because it is too large Load Diff

@ -14,8 +14,8 @@ while true; do
read -rp " ${QST} Are you sure you would like to remove ${COL_WHITE}Pi-hole${COL_NC}? [y/N] " yn
case ${yn} in
[Yy]* ) break;;
[Nn]* ) echo -e "\n ${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;;
[Nn]* ) echo -e "${OVER} ${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
done
@ -46,34 +46,24 @@ source "${setupVars}"
distro_check
# Install packages used by the Pi-hole
if [[ "${INSTALL_WEB}" == true ]]; then
# Install the Web dependencies
DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}" "${PIHOLE_WEB_DEPS[@]}")
# Otherwise,
else
# just install the Core dependencies
DEPS=("${INSTALLER_DEPS[@]}" "${PIHOLE_DEPS[@]}")
if [[ "${INSTALL_WEB_SERVER}" == true ]]; then
# Install the Web dependencies
DEPS+=("${PIHOLE_WEB_DEPS[@]}")
fi
# Compatability
if [ -x "$(command -v rpm)" ]; then
# Fedora Family
PKG_REMOVE="${PKG_MANAGER} remove -y"
package_check() {
rpm -qa | grep ^$1- > /dev/null
}
package_cleanup() {
${SUDO} ${PKG_MANAGER} -y autoremove
}
elif [ -x "$(command -v apt-get)" ]; then
if [ -x "$(command -v apt-get)" ]; then
# Debian Family
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
elif [ -x "$(command -v rpm)" ]; then
# Fedora Family
PKG_REMOVE="${PKG_MANAGER} remove -y"
package_check() {
rpm -qa | grep "^$1-" > /dev/null
}
else
echo -e " ${CROSS} OS distribution not supported"
@ -84,14 +74,13 @@ removeAndPurge() {
# Purge dependencies
echo ""
for i in "${DEPS[@]}"; do
package_check ${i} > /dev/null
if [[ "$?" -eq 0 ]]; then
if package_check "${i}" > /dev/null; then
while true; do
read -rp " ${QST} Do you wish to remove ${COL_WHITE}${i}${COL_NC} from your system? [Y/N] " yn
case ${yn} in
[Yy]* )
echo -ne " ${INFO} Removing ${i}...";
${SUDO} ${PKG_REMOVE} "${i}" &> /dev/null;
${SUDO} "${PKG_REMOVE} ${i}" &> /dev/null;
echo -e "${OVER} ${INFO} Removed ${i}";
break;;
[Nn]* ) echo -e " ${INFO} Skipped ${i}"; break;;
@ -103,14 +92,9 @@ removeAndPurge() {
done
# Remove dnsmasq config files
${SUDO} rm -f /etc/dnsmasq.conf /etc/dnsmasq.conf.orig /etc/dnsmasq.d/01-pihole.conf &> /dev/null
${SUDO} rm -f /etc/dnsmasq.conf /etc/dnsmasq.conf.orig /etc/dnsmasq.d/*-pihole*.conf &> /dev/null
echo -e " ${TICK} Removing dnsmasq config files"
# Take care of any additional package cleaning
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
}
@ -168,32 +152,42 @@ removeNoPurge() {
${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
${SUDO} userdel -r pihole 2> /dev/null
if [[ "$?" -eq 0 ]]; 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
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
Reinstall at any time: ${COL_WHITE}curl -sSL https://install.pi-hole.net | bash${COL_NC}

@ -15,20 +15,20 @@ export LC_ALL=C
coltable="/opt/pihole/COL_TABLE"
source "${coltable}"
regexconverter="/opt/pihole/wildcard_regex_converter.sh"
source "${regexconverter}"
basename="pihole"
PIHOLE_COMMAND="/usr/local/bin/${basename}"
piholeDir="/etc/${basename}"
piholeRepo="/etc/.${basename}"
adListFile="${piholeDir}/adlists.list"
adListDefault="${piholeDir}/adlists.default"
adListRepoDefault="${piholeRepo}/adlists.default"
whitelistFile="${piholeDir}/whitelist.txt"
blacklistFile="${piholeDir}/blacklist.txt"
wildcardFile="/etc/dnsmasq.d/03-pihole-wildcard.conf"
regexFile="${piholeDir}/regex.list"
adList="${piholeDir}/gravity.list"
blackList="${piholeDir}/black.list"
@ -44,6 +44,10 @@ preEventHorizon="list.preEventHorizon"
skipDownload="false"
resolver="pihole-FTL"
haveSourceUrls=true
# Source setupVars from install script
setupVars="${piholeDir}/setupVars.conf"
if [[ -f "${setupVars}" ]];then
@ -104,7 +108,7 @@ gravity_CheckDNSResolutionAvailable() {
fi
# Determine error output message
if pidof dnsmasq &> /dev/null; then
if pidof ${resolver} &> /dev/null; then
echo -e " ${CROSS} DNS resolution is currently unavailable"
else
echo -e " ${CROSS} DNS service is not running"
@ -129,20 +133,12 @@ gravity_CheckDNSResolutionAvailable() {
gravity_GetBlocklistUrls() {
echo -e " ${INFO} ${COL_BOLD}Neutrino emissions detected${COL_NC}..."
# Determine if adlists file needs handling
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
if [[ -f "${adListDefault}" ]] && [[ -f "${adListFile}" ]]; then
# Remove superceded $adListDefault file
rm "${adListDefault}" 2> /dev/null || \
echo -e " ${CROSS} Unable to remove ${adListDefault}"
fi
local str="Pulling blocklist source list into range"
echo -ne " ${INFO} ${str}..."
# Retrieve source URLs from $adListFile
# Logic: Remove comments and empty lines
mapfile -t sources <<< "$(grep -v -E "^(#|$)" "${adListFile}" 2> /dev/null)"
@ -158,11 +154,15 @@ gravity_GetBlocklistUrls() {
}' <<< "$(printf '%s\n' "${sources[@]}")" 2> /dev/null
)"
local str="Pulling blocklist source list into range"
if [[ -n "${sources[*]}" ]] && [[ -n "${sourceDomains[*]}" ]]; then
echo -e "${OVER} ${TICK} ${str}"
else
echo -e "${OVER} ${CROSS} ${str}"
gravity_Cleanup "error"
echo -e " ${INFO} No source list found, or it is empty"
echo ""
haveSourceUrls=false
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)
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?
"http"*)
*)
# Determine "Status:" output based on HTTP response
case "${httpCode}" in
"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)";;
"521") echo -e "${OVER} ${CROSS} ${str} Web Server Is Down (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;;
# 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
# Determine if the blocklist was downloaded and saved correctly
@ -280,8 +279,8 @@ gravity_ParseFileIntoDomains() {
# 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
# 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
#+ the right (IP address), otherwise grab the single field.
# Last awk command takes non-commented lines and if they have 2 fields, take the right field (the domain) and leave
# the left (IP address), otherwise grab the single field.
< ${source} 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
echo -ne " ${INFO} Format: URL"
awk '{
# Remove URL protocol, optional "username:password@", and ":?/;"
if ($0 ~ /[:?\/;]/) { gsub(/(^.*:\/\/(.*:.*@)?|[:?\/;].*)/, "", $0) }
# Remove lines which are only IPv4 addresses
if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { $0="" }
if ($0) { print $0 }
}' "${source}" 2> /dev/null > "${destination}"
awk '
# Remove URL scheme, optional "username:password@", and ":?/;"
# The scheme must be matched carefully to avoid blocking the wrong URL
# in cases like:
# http://www.evil.com?http://www.good.com
# See RFC 3986 section 3.1 for details.
/[:?\/;]/ { gsub(/(^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.*:.*@)?|[:?\/;].*)/, "", $0) }
# Skip lines which are only IPv4 addresses
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { next }
# Print if nonempty
length { print }
' "${source}" 2> /dev/null > "${destination}"
echo -e "${OVER} ${TICK} Format: URL"
else
@ -371,7 +375,9 @@ gravity_ConsolidateDownloadedBlocklists() {
local str lastLine
str="Consolidating blocklists"
if [[ "${haveSourceUrls}" == true ]]; then
echo -ne " ${INFO} ${str}..."
fi
# Empty $matterAndLight if it already exists, otherwise, create it
: > "${piholeDir}/${matterAndLight}"
@ -390,8 +396,9 @@ gravity_ConsolidateDownloadedBlocklists() {
fi
fi
done
if [[ "${haveSourceUrls}" == true ]]; then
echo -e "${OVER} ${TICK} ${str}"
fi
}
# Parse consolidated list into (filtered, unique) domains-only format
@ -399,24 +406,33 @@ gravity_SortAndFilterConsolidatedList() {
local str num
str="Extracting domains from blocklists"
if [[ "${haveSourceUrls}" == true ]]; then
echo -ne " ${INFO} ${str}..."
fi
# Parse into hosts file
gravity_ParseFileIntoDomains "${piholeDir}/${matterAndLight}" "${piholeDir}/${parsedMatter}"
# Format $parsedMatter line total as currency
num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${parsedMatter}")")
echo -e "${OVER} ${TICK} ${str}
${INFO} Number of domains being pulled in by gravity: ${COL_BLUE}${num}${COL_NC}"
if [[ "${haveSourceUrls}" == true ]]; then
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"
if [[ "${haveSourceUrls}" == true ]]; then
echo -ne " ${INFO} ${str}..."
fi
sort -u "${piholeDir}/${parsedMatter}" > "${piholeDir}/${preEventHorizon}"
echo -e "${OVER} ${TICK} ${str}"
if [[ "${haveSourceUrls}" == true ]]; then
echo -e "${OVER} ${TICK} ${str}"
# 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
@ -438,7 +454,7 @@ gravity_Whitelist() {
echo -e "${OVER} ${INFO} ${str}"
}
# Output count of blacklisted domains and wildcards
# Output count of blacklisted domains and regex filters
gravity_ShowBlockCount() {
local num
@ -447,13 +463,9 @@ gravity_ShowBlockCount() {
echo -e " ${INFO} Number of blacklisted domains: ${num}"
fi
if [[ -f "${wildcardFile}" ]]; then
num=$(grep -c "^" "${wildcardFile}")
# If IPv4 and IPv6 is used, divide total wildcard count by 2
if [[ -n "${IPV4_ADDRESS}" ]] && [[ -n "${IPV6_ADDRESS}" ]];then
num=$(( num/2 ))
fi
echo -e " ${INFO} Number of wildcard blocked domains: ${num}"
if [[ -f "${regexFile}" ]]; then
num=$(grep -c "^(?!#)" "${regexFile}")
echo -e " ${INFO} Number of regex filters: ${num}"
fi
}
@ -507,10 +519,10 @@ gravity_ParseBlacklistDomains() {
: > "${piholeDir}/${accretionDisc}"
if [[ -f "${piholeDir}/${whitelistMatter}" ]]; then
gravity_ParseDomainsIntoHosts "${piholeDir}/${whitelistMatter}" "${piholeDir}/${accretionDisc}"
mv "${piholeDir}/${whitelistMatter}" "${piholeDir}/${accretionDisc}"
else
# There was no whitelist file, so use preEventHorizon instead of whitelistMatter.
gravity_ParseDomainsIntoHosts "${piholeDir}/${preEventHorizon}" "${piholeDir}/${accretionDisc}"
mv "${piholeDir}/${preEventHorizon}" "${piholeDir}/${accretionDisc}"
fi
# Move the file over as /etc/pihole/gravity.list so dnsmasq can use it
@ -528,11 +540,9 @@ gravity_ParseUserDomains() {
if [[ ! -f "${blacklistFile}" ]]; then
return 0
fi
gravity_ParseDomainsIntoHosts "${blacklistFile}" "${blackList}.tmp"
# Copy the file over as /etc/pihole/black.list so dnsmasq can use it
mv "${blackList}.tmp" "${blackList}" 2> /dev/null || \
echo -e "\\n ${CROSS} Unable to move ${blackList##*/}.tmp to ${piholeDir}"
cp "${blacklistFile}" "${blackList}" 2> /dev/null || \
echo -e "\\n ${CROSS} Unable to move ${blacklistFile##*/} to ${piholeDir}"
}
# Trap Ctrl-C
@ -567,7 +577,7 @@ gravity_Cleanup() {
echo -e "${OVER} ${TICK} ${str}"
# Only restart DNS service if offline
if ! pidof dnsmasq &> /dev/null; then
if ! pidof ${resolver} &> /dev/null; then
"${PIHOLE_COMMAND}" restartdns
dnsWasOffline=true
fi
@ -616,7 +626,9 @@ if [[ "${skipDownload}" == false ]]; then
# Gravity needs to download blocklists
gravity_CheckDNSResolutionAvailable
gravity_GetBlocklistUrls
if [[ "${haveSourceUrls}" == true ]]; then
gravity_SetDownloadOptions
fi
gravity_ConsolidateDownloadedBlocklists
gravity_SortAndFilterConsolidatedList
else
@ -631,6 +643,7 @@ if [[ "${skipDownload}" == false ]] || [[ "${listType}" == "whitelist" ]]; then
gravity_Whitelist
fi
convert_wildcard_to_regex
gravity_ShowBlockCount
# 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"
source "${colfile}"
resolver="pihole-FTL"
# Must be root to use this tool
if [[ ! $EUID -eq 0 ]];then
if [[ -x "$(command -v sudo)" ]]; then
@ -31,17 +33,7 @@ webpageFunc() {
exit 0
}
whitelistFunc() {
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
exit 0
}
blacklistFunc() {
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
exit 0
}
wildcardFunc() {
listFunc() {
"${PI_HOLE_SCRIPT_DIR}"/list.sh "$@"
exit 0
}
@ -69,7 +61,8 @@ flushFunc() {
}
updatePiholeFunc() {
"${PI_HOLE_SCRIPT_DIR}"/update.sh
shift
"${PI_HOLE_SCRIPT_DIR}"/update.sh "$@"
exit 0
}
@ -83,230 +76,9 @@ updateGravityFunc() {
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() {
shift
local options="$*" adlist="" all="" exact="" blockpage="" matchType="match"
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
"${PI_HOLE_SCRIPT_DIR}"/query.sh "$@"
exit 0
}
@ -332,18 +104,18 @@ restartDNS() {
local svcOption svc str output status
svcOption="${1:-}"
# Determine if we should reload or restart dnsmasq
# Determine if we should reload or restart restart
if [[ "${svcOption}" =~ "reload" ]]; then
# Using SIGHUP will NOT re-read any *.conf files
svc="killall -s SIGHUP dnsmasq"
svc="killall -s SIGHUP ${resolver}"
else
# Get PID of dnsmasq to determine if it needs to start or restart
if pidof dnsmasq &> /dev/null; then
# Get PID of resolver to determine if it needs to start or restart
if pidof pihole-FTL &> /dev/null; then
svcOption="restart"
else
svcOption="start"
fi
svc="service dnsmasq ${svcOption}"
svc="service ${resolver} ${svcOption}"
fi
# Print output to Terminal, but not to Web Admin
@ -359,9 +131,6 @@ restartDNS() {
[[ ! -t 1 ]] && local OVER=""
echo -e "${OVER} ${CROSS} ${output}"
fi
# Send signal to FTL to have it re-parse the gravity files
killall -s SIGHUP pihole-FTL
}
piholeEnable() {
@ -476,7 +245,7 @@ statusFunc() {
local addnConfigs
# 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
echo -e " ${TICK} DNS service is running"
fi
@ -516,6 +285,13 @@ statusFunc() {
}
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"
# Retrieve IPv4/6 addresses
@ -541,12 +317,13 @@ Switch Pi-hole subsystems to a different Github branch
Repositories:
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
Branches:
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
fi
@ -599,7 +376,8 @@ Add '-h' after specific commands for more information on usage
Whitelist/Blacklist Options:
-w, whitelist Whitelist 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
Debugging Options:
@ -610,8 +388,8 @@ Debugging Options:
-t, tail View the live output of the Pi-hole log
Options:
-a, admin Admin Console options
Add '-h' for more info on admin console usage
-a, admin Web interface options
Add '-h' for more info on Web Interface usage
-c, chronometer Calculates stats and displays to an LCD
Add '-h' for more info on chronometer usage
-g, updateGravity Update the list of ad-serving domains
@ -621,7 +399,8 @@ Options:
-q, query Query the adlists for a specified domain
Add '-h' for more info on query usage
-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
uninstall Uninstall Pi-hole from your system
status Display the running status of Pi-hole subsystems
@ -640,12 +419,13 @@ fi
# Handle redirecting to specific functions based on arguments
case "${1}" in
"-w" | "whitelist" ) whitelistFunc "$@";;
"-b" | "blacklist" ) blacklistFunc "$@";;
"-wild" | "wildcard" ) wildcardFunc "$@";;
"-w" | "whitelist" ) listFunc "$@";;
"-b" | "blacklist" ) listFunc "$@";;
"--wild" | "wildcard" ) listFunc "$@";;
"--regex" | "regex" ) listFunc "$@";;
"-d" | "debug" ) debugFunc "$@";;
"-f" | "flush" ) flushFunc "$@";;
"-up" | "updatePihole" ) updatePiholeFunc;;
"-up" | "updatePihole" ) updatePiholeFunc "$@";;
"-r" | "reconfigure" ) reconfigurePiholeFunc;;
"-g" | "updateGravity" ) updateGravityFunc "$@";;
"-c" | "chronometer" ) chronometerFunc "$@";;

@ -3,3 +3,4 @@ pytest
pytest-xdist
pytest-cov
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 testinfra
from textwrap import dedent
check_output = testinfra.get_backend(
"local://"
).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
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):
cmd = self.get_command(command, *args)
if self.user is not None:
@ -22,12 +38,18 @@ def Pihole(Docker):
return out
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
@pytest.fixture
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?"
docker_run = "docker run {} {} {}".format(args, image, cmd)
docker_id = check_output(docker_run)
@ -40,22 +62,95 @@ def Docker(request, args, image, cmd):
docker_container.id = docker_id
return docker_container
@pytest.fixture
def args(request):
''' -t became required when tput began being used '''
'''
-t became required when tput began being used
'''
return '-t -d'
@pytest.fixture(params=['debian', 'centos'])
@pytest.fixture(params=['debian', 'centos', 'fedora'])
def tag(request):
''' consumed by image to make the test matrix '''
'''
consumed by image to make the test matrix
'''
return request.param
@pytest.fixture()
def image(request, tag):
''' built by test_000_build_containers.py '''
'''
built by test_000_build_containers.py
'''
return 'pytest_pihole:{}'.format(tag)
@pytest.fixture()
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'
# 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://"
).get_module("Command").run
@pytest.mark.parametrize("image,tag", [
('test/debian.Dockerfile', 'pytest_pihole:debian'),
('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):
build_cmd = run_local('docker build -f {} -t {} .'.format(image, tag))
if build_cmd.rc != 0:

@ -1,22 +1,38 @@
import pytest
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):
''' currently update_dialogs sources setupVars with a dot,
'''
currently update_dialogs sources setupVars with a dot,
then various other functions use the variables.
This confirms the sourced variables are in scope between functions '''
This confirms the sourced variables are in scope between functions
'''
setup_var_file = 'cat <<EOF> /etc/pihole/setupVars.conf\n'
for k, v in SETUPVARS.iteritems():
setup_var_file += "{}={}\n".format(k, v)
@ -46,9 +62,13 @@ def test_setupVars_are_sourced_to_global_scope(Pihole):
for k, v in SETUPVARS.iteritems():
assert "{}={}".format(k, v) in output
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
'''
# 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)
Pihole.run(set_setup_vars).stdout
@ -70,8 +90,11 @@ def test_setupVars_saved_to_file(Pihole):
for k, v in SETUPVARS.iteritems():
assert "{}={}".format(k, v) in output
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
mock_command('firewall-cmd', {'*': ('running', 0)}, Pihole)
# Whiptail dialog returns Ok for user prompt
@ -80,26 +103,37 @@ def test_configureFirewall_firewalld_running_no_errors(Pihole):
source /opt/pihole/basic-install.sh
configureFirewall
''')
expected_stdout = 'Configuring FirewallD for httpd and dnsmasq'
expected_stdout = 'Configuring FirewallD for httpd and pihole-FTL'
assert expected_stdout in configureFirewall.stdout
firewall_calls = Pihole.run('cat /var/log/firewall-cmd').stdout
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
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
mock_command('firewall-cmd', {'*': ('not running', '1')}, Pihole)
configureFirewall = Pihole.run('''
source /opt/pihole/basic-install.sh
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
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
mock_command('firewall-cmd', {'*': ('running', 0)}, Pihole)
# Whiptail dialog returns Cancel for user prompt
@ -111,6 +145,7 @@ def test_configureFirewall_firewalld_enabled_declined_no_errors(Pihole):
expected_stdout = 'Not installing firewall rulesets.'
assert expected_stdout in configureFirewall.stdout
def test_configureFirewall_no_firewall(Pihole):
''' confirms firewall skipped no daemon is running '''
configureFirewall = Pihole.run('''
@ -120,8 +155,12 @@ def test_configureFirewall_no_firewall(Pihole):
expected_stdout = 'No active firewall detected'
assert expected_stdout in configureFirewall.stdout
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
mock_command('iptables', {'*': ('', '0')}, Pihole)
# modinfo returns always true (ip_tables module check)
@ -135,9 +174,14 @@ def test_configureFirewall_IPTables_enabled_declined_no_errors(Pihole):
expected_stdout = 'Not installing firewall rulesets.'
assert expected_stdout in configureFirewall.stdout
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
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)
mock_command('modinfo', {'*': ('', '0')}, Pihole)
@ -150,14 +194,42 @@ def test_configureFirewall_IPTables_enabled_rules_exist_no_errors(Pihole):
expected_stdout = 'Installing new IPTables firewall rulesets'
assert expected_stdout in configureFirewall.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
assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT' not in firewall_calls
assert 'iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT' not in firewall_calls
# General call type occurances
assert len(re.findall(r'iptables -S', firewall_calls)) == 1
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):
''' 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)
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)
mock_command('modinfo', {'*': ('', '0')}, Pihole)
# Whiptail dialog returns Cancel for user prompt
@ -169,52 +241,160 @@ def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole):
expected_stdout = 'Installing new IPTables firewall rulesets'
assert expected_stdout in configureFirewall.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
assert 'iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT' in firewall_calls
assert 'iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT' in firewall_calls
# General call type occurances
assert len(re.findall(r'iptables -S', firewall_calls)) == 1
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):
''' 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('''
source /opt/pihole/basic-install.sh
installPiholeWeb
''')
assert info_box + ' Installing blocking page...' in installWeb.stdout
assert tick_box + ' Creating directory for blocking page, and copying files' in installWeb.stdout
assert cross_box + ' Backing up index.lighttpd.html' in installWeb.stdout
assert 'No default index.lighttpd.html file found... not backing up' in installWeb.stdout
assert tick_box + ' Installing sudoer file' in installWeb.stdout
expected_stdout = info_box + ' Installing blocking page...'
assert expected_stdout in installWeb.stdout
expected_stdout = tick_box + (' Creating directory for blocking page, '
'and copying files')
assert expected_stdout in installWeb.stdout
expected_stdout = 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
assert 'index.php' in web_directory
assert 'blockingpage.css' in web_directory
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('''
source /opt/pihole/basic-install.sh
distro_check
update_package_cache
''')
assert tick_box + ' Update local cache of available packages' in updateCache.stdout
assert 'Error: Unable to update package cache.' not in updateCache.stdout
expected_stdout = tick_box + ' Update local cache of available packages'
assert expected_stdout in updateCache.stdout
assert 'error' not in updateCache.stdout.lower()
def test_update_package_cache_failure_no_errors(Pihole):
''' confirms package cache was not updated'''
'''
confirms package cache was not updated
'''
mock_command('apt-get', {'update': ('', '1')}, Pihole)
updateCache = Pihole.run('''
source /opt/pihole/basic-install.sh
distro_check
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
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_command('uname', {'-m': ('aarch64', '0')}, Pihole)
# 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('''
source /opt/pihole/basic-install.sh
FTLdetect
@ -226,8 +406,11 @@ def test_FTL_detect_aarch64_no_errors(Pihole):
expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout
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_command('uname', {'-m': ('armv6l', '0')}, Pihole)
# mock ldd to respond with aarch64 shared library
@ -238,13 +421,17 @@ def test_FTL_detect_armv6l_no_errors(Pihole):
''')
expected_stdout = info_box + ' FTL Checks...'
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
expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout
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_command('uname', {'-m': ('armv7l', '0')}, Pihole)
# mock ldd to respond with aarch64 shared library
@ -260,8 +447,11 @@ def test_FTL_detect_armv7l_no_errors(Pihole):
expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout
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('''
source /opt/pihole/basic-install.sh
FTLdetect
@ -273,6 +463,7 @@ def test_FTL_detect_x86_64_no_errors(Pihole):
expected_stdout = tick_box + ' Downloading and Installing FTL'
assert expected_stdout in detectPlatform.stdout
def test_FTL_detect_unknown_no_errors(Pihole):
''' confirms only generic package is downloaded for FTL engine '''
# mock uname to return generic platform
@ -284,8 +475,11 @@ def test_FTL_detect_unknown_no_errors(Pihole):
expected_stdout = 'Not able to detect architecture (unknown: mips)'
assert expected_stdout in detectPlatform.stdout
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
download_binary = Pihole.run('''
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'
assert expected_stdout in download_binary.stdout
error = 'Error: Download of binary from Github failed'
assert error not in download_binary.stdout
error = 'Error: URL not found'
assert error not in download_binary.stdout
assert 'error' not in download_binary.stdout.lower()
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
download_binary = Pihole.run('''
source /opt/pihole/basic-install.sh
@ -310,8 +504,11 @@ def test_FTL_download_unknown_fails_no_errors(Pihole):
error = 'Error: URL not found'
assert error in download_binary.stdout
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('''
source /opt/pihole/basic-install.sh
FTLdetect
@ -320,8 +517,11 @@ def test_FTL_binary_installed_and_responsive_no_errors(Pihole):
expected_stdout = 'v'
assert expected_stdout in installed_binary.stdout
# def test_FTL_support_files_installed(Pihole):
# ''' confirms FTL support files are installed '''
# '''
# confirms FTL support files are installed
# '''
# support_files = Pihole.run('''
# source /opt/pihole/basic-install.sh
# 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 /var/log/pihole-FTL.log' in support_files.stdout
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_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('''
source /opt/pihole/basic-install.sh
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
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_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('''
source /opt/pihole/basic-install.sh
useIPv6dialog
@ -356,10 +581,22 @@ def test_IPv6_only_ULA(Pihole):
expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout
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_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('''
source /opt/pihole/basic-install.sh
useIPv6dialog
@ -367,10 +604,23 @@ def test_IPv6_only_GUA(Pihole):
expected_stdout = 'Found IPv6 GUA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout
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_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('''
source /opt/pihole/basic-install.sh
useIPv6dialog
@ -378,61 +628,26 @@ def test_IPv6_GUA_ULA_test(Pihole):
expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads'
assert expected_stdout in detectPlatform.stdout
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_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('''
source /opt/pihole/basic-install.sh
useIPv6dialog
''')
expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads'
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
run_local = testinfra.get_backend(
"local://"
).get_module("Command").run
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)
print 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