diff options
-rw-r--r-- | README.md | 12 | ||||
-rwxr-xr-x | advanced/Scripts/chronometer.sh | 80 | ||||
-rwxr-xr-x | advanced/Scripts/list.sh | 115 | ||||
-rwxr-xr-x | advanced/Scripts/piholeDebug.sh | 20 | ||||
-rwxr-xr-x | advanced/Scripts/piholeLogFlush.sh | 7 | ||||
-rwxr-xr-x | advanced/Scripts/webpage.sh | 58 | ||||
-rw-r--r-- | advanced/index.php | 23 | ||||
-rw-r--r-- | advanced/logrotate | 9 | ||||
-rw-r--r-- | advanced/pihole.cron | 3 | ||||
-rwxr-xr-x | automated install/basic-install.sh | 460 | ||||
-rwxr-xr-x | gravity.sh | 6 | ||||
-rwxr-xr-x | pihole | 53 | ||||
-rw-r--r-- | test/test_automated_install.py | 220 |
13 files changed, 704 insertions, 362 deletions
@@ -42,7 +42,7 @@ _If you wish to read over the script before running it, run `nano basic-install. ``` git clone --depth 1 https://github.com/pi-hole/pi-hole.git Pi-hole -cd Pi-hole/automated_installer/ +cd Pi-hole/automated\ install/ bash basic-install.sh ``` @@ -55,17 +55,11 @@ bash basic-install.sh Once installed, [configure your router to have **DHCP clients use the Pi as their DNS server**](https://discourse.pi-hole.net/t/how-do-i-configure-my-devices-to-use-pi-hole-as-their-dns-server/245) and then any device that connects to your network will have ads blocked without any further configuration. Alternatively, you can manually set each device to [use the Raspberry Pi as its DNS server](http://pi-hole.net/faq/how-do-i-use-the-pi-hole-as-my-dns-server/). -## Installing the Pi-hole (Click to Watch!) +## What is Pi-hole and how do I install it? <p align="center"> -<a href=https://www.youtube.com/watch?v=TzFLJqUeirA><img src="https://assets.pi-hole.net/static/global.png"></a> +<a href=https://www.youtube.com/watch?v=vKWjx1AQYgs><img src="https://assets.pi-hole.net/static/global.png"></a> </p> -## Would you like to know more? - -**Watch the 60-second video below to get a quick overview** -<p align="center"> -<a href=https://youtu.be/9Eti3xibiho><img src="https://assets.pi-hole.net/static/blackhole_web.png"></a> -</p> ## Get Help Or Connect With Us On The Web diff --git a/advanced/Scripts/chronometer.sh b/advanced/Scripts/chronometer.sh index a28bb868..93b0cbb1 100755 --- a/advanced/Scripts/chronometer.sh +++ b/advanced/Scripts/chronometer.sh @@ -17,56 +17,21 @@ gravity="/etc/pihole/gravity.list" . /etc/pihole/setupVars.conf -CalcBlockedDomains() { - if [ -e "${gravity}" ]; then - # if BOTH IPV4 and IPV6 are in use, then we need to divide total domains by 2. - if [[ -n "${IPV4_ADDRESS}" && -n "${IPV6_ADDRESS}" ]]; then - blockedDomainsTotal=$(wc -l /etc/pihole/gravity.list | awk '{print $1/2}') - else - # only one is set. - blockedDomainsTotal=$(wc -l /etc/pihole/gravity.list | awk '{print $1}') - fi - else - blockedDomainsTotal="Err." - fi -} - -CalcQueriesToday() { - if [ -e "${piLog}" ]; then - queriesToday=$(awk '/query\[/ {print $6}' < "${piLog}" | wc -l) - else - queriesToday="Err." - fi -} - -CalcblockedToday() { - if [ -e "${piLog}" ] && [ -e "${gravity}" ];then - blockedToday=$(awk '/\/etc\/pihole\/gravity.list/ && !/address/ {print $6}' < "${piLog}" | wc -l) - else - blockedToday="Err." - fi -} - -CalcPercentBlockedToday() { - if [ "${queriesToday}" != "Err." ] && [ "${blockedToday}" != "Err." ]; then - if [ "${queriesToday}" != 0 ]; then #Fixes divide by zero error :) - #scale 2 rounds the number down, so we'll do scale 4 and then trim the last 2 zeros - percentBlockedToday=$(echo "scale=4; ${blockedToday}/${queriesToday}*100" | bc) - percentBlockedToday=$(sed 's/.\{2\}$//' <<< "${percentBlockedToday}") - else - percentBlockedToday=0 - fi - fi +# Borrowed/modified from https://gist.github.com/cjus/1047794 +function GetJSONValue { + retVal=$(echo $1 | sed 's/\\\\\//\//g' | \ + sed 's/[{}]//g' | \ + awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}' | \ + sed 's/\"\:\"/\|/g' | \ + sed 's/[\,]/ /g' | \ + sed 's/\"//g' | \ + grep -w $2) + echo ${retVal##*|} } outputJSON() { - CalcQueriesToday - CalcblockedToday - CalcPercentBlockedToday - - CalcBlockedDomains - - printf '{"domains_being_blocked":"%s","dns_queries_today":"%s","ads_blocked_today":"%s","ads_percentage_today":"%s"}\n' "$blockedDomainsTotal" "$queriesToday" "$blockedToday" "$percentBlockedToday" + json=$(curl -s -X GET http://127.0.0.1/admin/api.php?summaryRaw) + echo ${json} } normalChrono() { @@ -87,22 +52,17 @@ normalChrono() { # Uncomment to continually read the log file and display the current domain being blocked #tail -f /var/log/pihole.log | awk '/\/etc\/pihole\/gravity.list/ {if ($7 != "address" && $7 != "name" && $7 != "/etc/pihole/gravity.list") print $7; else;}' - #uncomment next 4 lines to use original query count calculation - #today=$(date "+%b %e") - #todaysQueryCount=$(cat /var/log/pihole.log | grep "$today" | awk '/query/ {print $7}' | wc -l) - #todaysQueryCountV4=$(cat /var/log/pihole.log | grep "$today" | awk '/query/ && /\[A\]/ {print $7}' | wc -l) - #todaysQueryCountV6=$(cat /var/log/pihole.log | grep "$today" | awk '/query/ && /\[AAAA\]/ {print $7}' | wc -l) - + json=$(curl -s -X GET http://127.0.0.1/admin/api.php?summaryRaw) - CalcQueriesToday - CalcblockedToday - CalcPercentBlockedToday + domains=$(printf "%'.f" $(GetJSONValue ${json} "domains_being_blocked")) #add commas in + queries=$(printf "%'.f" $(GetJSONValue ${json} "dns_queries_today")) + blocked=$(printf "%'.f" $(GetJSONValue ${json} "ads_blocked_today")) + LC_NUMERIC=C percentage=$(printf "%0.2f\n" $(GetJSONValue ${json} "ads_percentage_today")) #2 decimal places - CalcBlockedDomains + echo "Blocking: ${domains}" + echo "Queries: ${queries}" - echo "Blocking: ${blockedDomainsTotal}" - echo "Queries: ${queriesToday}" #same total calculation as dashboard - echo "Pi-holed: ${blockedToday} (${percentBlockedToday}%)" + echo "Pi-holed: ${blocked} (${percentage}%)" sleep 5 done diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index bb3a8a43..c916e021 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -15,6 +15,7 @@ basename=pihole piholeDir=/etc/${basename} whitelist=${piholeDir}/whitelist.txt blacklist=${piholeDir}/blacklist.txt +readonly wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf" reload=false addmode=true verbose=true @@ -47,13 +48,17 @@ helpFunc() { ::: -h, --help Show this help dialog ::: -l, --list Display your ${word}listed domains EOM +if [[ "${letter}" == "b" ]]; then + echo "::: -wild, --wildcard Add whitecard entry (only blacklist)" +fi exit 0 } EscapeRegexp() { # This way we may safely insert an arbitrary # string in our regular expressions - echo $* | sed "s/[]\\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" + # Also remove leading "." if present + echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" } HandleOther(){ @@ -61,7 +66,7 @@ HandleOther(){ domain=$(sed -e "y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/" <<< "$1") #check validity of domain - validDomain=$(perl -ne "print if /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/" <<< "$domain") + validDomain=$(echo "${domain}" | perl -lne 'print if /^(?!.*[^a-z0-9-\.].*)\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)*[a-z]{2,63}\b/') if [ -z "${validDomain}" ]; then echo "::: $1 is not a valid argument or domain name" else @@ -89,22 +94,51 @@ AddDomain() { list="$2" domain=$(EscapeRegexp "$1") - bool=true - #Is the domain in the list we want to add it to? - grep -Ex -q "${domain}" ${list} > /dev/null 2>&1 || bool=false - - if [[ "${bool}" == false ]]; then - #domain not found in the whitelist file, add it! - if [[ "${verbose}" == true ]]; then - echo "::: Adding $1 to $list..." - fi - reload=true - # Add it to the list we want to add it to - echo "$1" >> ${list} - else - if [[ "${verbose}" == true ]]; then - echo "::: ${1} already exists in ${list}, no need to add!" - fi + if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then + + bool=true + #Is the domain in the list we want to add it to? + grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false + + if [[ "${bool}" == false ]]; then + #domain not found in the whitelist file, add it! + if [[ "${verbose}" == true ]]; then + echo "::: Adding $1 to $list..." + fi + reload=true + # Add it to the list we want to add it to + echo "$1" >> "${list}" + else + if [[ "${verbose}" == true ]]; then + echo "::: ${1} already exists in ${list}, no need to add!" + fi + fi + + elif [[ "${list}" == "${wildcardlist}" ]]; then + + source "${piholeDir}/setupVars.conf" + #Remove the /* from the end of the IPv4addr. + IPV4_ADDRESS=${IPV4_ADDRESS%/*} + IPV6_ADDRESS=${IPV6_ADDRESS} + + bool=true + #Is the domain in the list? + grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false + + if [[ "${bool}" == false ]]; then + if [[ "${verbose}" == true ]]; then + echo "::: Adding $1 to wildcard blacklist..." + fi + reload=true + echo "address=/$1/${IPV4_ADDRESS}" >> "${wildcardlist}" + if [[ ${#IPV6_ADDRESS} > 0 ]] ; then + echo "address=/$1/${IPV6_ADDRESS}" >> "${wildcardlist}" + fi + else + if [[ "${verbose}" == true ]]; then + echo "::: ${1} already exists in wildcard blacklist, no need to add!" + fi + fi fi } @@ -112,18 +146,38 @@ RemoveDomain() { list="$2" domain=$(EscapeRegexp "$1") - bool=true - #Is it in the list? Logic follows that if its whitelisted it should not be blacklisted and vice versa - grep -Ex -q "${domain}" ${list} > /dev/null 2>&1 || bool=false - if [[ "${bool}" == true ]]; then - # Remove it from the other one - echo "::: Removing $1 from $list..." - # /I flag: search case-insensitive - sed -i "/${domain}/Id" ${list} - reload=true - else - if [[ "${verbose}" == true ]]; then - echo "::: ${1} does not exist in ${list}, no need to remove!" + if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then + + bool=true + #Is it in the list? Logic follows that if its whitelisted it should not be blacklisted and vice versa + grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false + if [[ "${bool}" == true ]]; then + # Remove it from the other one + echo "::: Removing $1 from $list..." + # /I flag: search case-insensitive + sed -i "/${domain}/Id" "${list}" + reload=true + else + if [[ "${verbose}" == true ]]; then + echo "::: ${1} does not exist in ${list}, no need to remove!" + fi + fi + + elif [[ "${list}" == "${wildcardlist}" ]]; then + + bool=true + #Is it in the list? + grep -e "address=\/${domain}\/" "${wildcardlist}" > /dev/null 2>&1 || bool=false + if [[ "${bool}" == true ]]; then + # Remove it from the other one + echo "::: Removing $1 from $list..." + # /I flag: search case-insensitive + sed -i "/address=\/${domain}/Id" "${list}" + reload=true + else + if [[ "${verbose}" == true ]]; then + echo "::: ${1} does not exist in ${list}, no need to remove!" + fi fi fi } @@ -153,6 +207,7 @@ for var in "$@"; do case "${var}" in "-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";; "-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";; + "-wild" | "wildcard" ) listMain="${wildcardlist}";; "-nr"| "--noreload" ) reload=false;; "-d" | "--delmode" ) addmode=false;; "-f" | "--force" ) force=true;; diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index d0e60177..f763d2b8 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -122,6 +122,13 @@ version_check() { && log_echo -r "${light_ver}" || (log_echo "lighttpd not installed." && error_found=1) local php_ver="$(php -v |& head -n1)" \ && log_echo -r "${php_ver}" || (log_echo "PHP not installed." && error_found=1) + + (local pi_hole_branch="$(cd /etc/.pihole/ && git rev-parse --abbrev-ref HEAD)" && log_echo -r "Pi-hole branch: ${pi_hole_branch}") || log_echo "Unable to obtain Pi-hole branch" + (local pi_hole_rev="$(cd /etc/.pihole/ && git describe --long --dirty --tags)" && log_echo -r "Pi-hole rev: ${pi_hole_rev}") || log_echo "Unable to obtain Pi-hole revision" + + (local admin_branch="$(cd /var/www/html/admin && git rev-parse --abbrev-ref HEAD)" && log_echo -r "AdminLTE branch: ${admin_branch}") || log_echo "Unable to obtain AdminLTE branch" + (local admin_rev="$(cd /var/www/html/admin && git describe --long --dirty --tags)" && log_echo -r "AdminLTE rev: ${admin_rev}") || log_echo "Unable to obtain AdminLTE revision" + return "${error_found}" } @@ -354,10 +361,21 @@ files_check "${ADLISTFILE}" header_write "Analyzing gravity.list" - gravity_length=$(wc -l "${GRAVITYFILE}") \ + gravity_length=$(grep -c ^ "${GRAVITYFILE}") \ && log_write "${GRAVITYFILE} is ${gravity_length} lines long." \ || log_echo "Warning: No gravity.list file found!" +header_write "Analyzing pihole.log" + + pihole_length=$(grep -c ^ "${PIHOLELOG}") \ + && log_write "${PIHOLELOG} is ${pihole_length} lines long." \ + || log_echo "Warning: No pihole.log file found!" + + pihole_size=$(du -h "${PIHOLELOG}" | awk '{ print $1 }') \ + && log_write "${PIHOLELOG} is ${pihole_size}." \ + || log_echo "Warning: No pihole.log file found!" + + # Continuously append the pihole.log file to the pihole_debug.log file dumpPiHoleLog() { trap '{ echo -e "\n::: Finishing debug write from interrupt... Quitting!" ; exit 1; }' INT diff --git a/advanced/Scripts/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index 8b16e3d5..d8f4fd79 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -11,5 +11,10 @@ # (at your option) any later version. echo -n "::: Flushing /var/log/pihole.log ..." -echo " " > /var/log/pihole.log +# Test if logrotate is available on this system +if command -v /usr/sbin/logrotate &> /dev/null; then + /usr/sbin/logrotate --force /etc/pihole/logrotate +else + echo " " > /var/log/pihole.log +fi echo "... done!" diff --git a/advanced/Scripts/webpage.sh b/advanced/Scripts/webpage.sh index acf8eca9..02610d85 100755 --- a/advanced/Scripts/webpage.sh +++ b/advanced/Scripts/webpage.sh @@ -92,12 +92,17 @@ SetWebPassword(){ ProcessDNSSettings() { source "${setupVars}" - delete_dnsmasq_setting "server=" - add_dnsmasq_setting "server" "${PIHOLE_DNS_1}" - - if [[ "${PIHOLE_DNS_2}" != "" ]]; then - add_dnsmasq_setting "server" "${PIHOLE_DNS_2}" - fi + delete_dnsmasq_setting "server" + + COUNTER=1 + while [[ 1 ]]; do + var=PIHOLE_DNS_${COUNTER} + if [ -z "${!var}" ]; then + break; + fi + add_dnsmasq_setting "server" "${!var}" + let COUNTER=COUNTER+1 + done delete_dnsmasq_setting "domain-needed" @@ -111,31 +116,45 @@ ProcessDNSSettings() { add_dnsmasq_setting "bogus-priv" fi + delete_dnsmasq_setting "dnssec" + delete_dnsmasq_setting "trust-anchor=" + + if [[ "${DNSSEC}" == true ]]; then + echo "dnssec +trust-anchor=.,19036,8,2,49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5 +" >> "${dnsmasqconfig}" + fi + } SetDNSServers(){ # Save setting to file - change_setting "PIHOLE_DNS_1" "${args[2]}" - - if [[ "${args[3]}" != "none" ]]; then - change_setting "PIHOLE_DNS_2" "${args[3]}" - else - change_setting "PIHOLE_DNS_2" "" - fi - - if [[ "${args[4]}" == "domain-needed" ]]; then + delete_setting "PIHOLE_DNS" + IFS=',' read -r -a array <<< "${args[2]}" + for index in "${!array[@]}" + do + add_setting "PIHOLE_DNS_$((index+1))" "${array[index]}" + done + + if [[ "${args[3]}" == "domain-needed" ]]; then change_setting "DNS_FQDN_REQUIRED" "true" else change_setting "DNS_FQDN_REQUIRED" "false" fi - if [[ "${args[4]}" == "bogus-priv" || "${args[5]}" == "bogus-priv" ]]; then + if [[ "${args[4]}" == "bogus-priv" ]]; then change_setting "DNS_BOGUS_PRIV" "true" else change_setting "DNS_BOGUS_PRIV" "false" fi + if [[ "${args[5]}" == "dnssec" ]]; then + change_setting "DNSSEC" "true" + else + change_setting "DNSSEC" "false" + fi + ProcessDNSSettings # Restart dnsmasq to load new configuration @@ -213,10 +232,13 @@ dhcp-authoritative dhcp-range=${DHCP_START},${DHCP_END},${leasetime} dhcp-option=option:router,${DHCP_ROUTER} dhcp-leasefile=/etc/pihole/dhcp.leases -domain=${PIHOLE_DOMAIN} #quiet-dhcp " > "${dhcpconfig}" +if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then + echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" +fi + if [[ "${DHCP_IPv6}" == "true" ]]; then echo "#quiet-dhcp6 #enable-ra @@ -227,7 +249,7 @@ ra-param=*,0,0 fi else - rm "${dhcpconfig}" + rm "${dhcpconfig}" &> /dev/null fi } diff --git a/advanced/index.php b/advanced/index.php index 1434025a..c076f92d 100644 --- a/advanced/index.php +++ b/advanced/index.php @@ -74,7 +74,9 @@ if($uri == "/") <a class='safe33' id="whitelisting">Whitelist this page</a> <a class='safe33' href='javascript:window.close()'>Close window</a> </div> - <div style="width: 98%; text-align: center; padding: 10px;" hidden="true" id="whitelistingform">Password required!<br/> + <div style="width: 98%; text-align: center; padding: 10px;" hidden="true" id="whitelistingform"> + <p>Note that whitelisting domains which are blocked using the wildcard method won't work.</p> + <p>Password required!</p><br/> <form> <input name="list" type="hidden" value="white"><br/> Domain:<br/> @@ -88,6 +90,16 @@ if($uri == "/") </main> <footer>Generated <?php echo date('D g:i A, M d'); ?> by Pi-hole <?php echo $piHoleVersion; ?></footer> <script src="http://pi.hole/admin/scripts/vendor/jquery.min.js"></script> +<script> +// Create event for when the output is appended to +(function($) { + var origAppend = $.fn.append; + + $.fn.append = function () { + return origAppend.apply(this, arguments).trigger("append"); + }; +})(jQuery); +</script> <script src="http://pi.hole/admin/scripts/pi-hole/js/queryads.js"></script> <script> function inIframe () { @@ -115,6 +127,15 @@ else $( "#whitelisting" ).on( "click", function(){ $( "#whitelistingform" ).removeAttr( "hidden" ); }); +// Remove whitelist functionality if the domain was blocked because of a wildcard +$( "#output" ).bind("append", function(){ + if($( "#output" ).contents()[0].data.indexOf("Wildcard blocking") !== -1) + { + $( "#whitelisting" ).hide(); + $( "#whitelistingform" ).hide(); + } +}); + function add() { var domain = $("#domain"); var pw = $("#pw"); diff --git a/advanced/logrotate b/advanced/logrotate new file mode 100644 index 00000000..e9be016d --- /dev/null +++ b/advanced/logrotate @@ -0,0 +1,9 @@ +/var/log/pihole.log { + daily + copytruncate + rotate 5 + compress + delaycompress + notifempty + nomail +} diff --git a/advanced/pihole.cron b/advanced/pihole.cron index 8311acfb..854a9459 100644 --- a/advanced/pihole.cron +++ b/advanced/pihole.cron @@ -23,4 +23,7 @@ # Pi-hole: Flush the log daily at 00:00 so it doesn't get out of control # Stats will be viewable in the Web interface thanks to the cron job above +# The flush script will use logrotate if available 00 00 * * * root PATH="$PATH:/usr/local/bin/" pihole flush + +@reboot root /usr/sbin/logrotate /etc/pihole/logrotate diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 0307bd2e..9fed0bca 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -27,7 +27,8 @@ webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git" webInterfaceDir="/var/www/html/admin" piholeGitUrl="https://github.com/pi-hole/pi-hole.git" PI_HOLE_LOCAL_REPO="/etc/.pihole" -PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version) +PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage) +PI_HOLE_INSTALL_DIR="/opt/pihole" useUpdateVars=false IPV4_ADDRESS="" @@ -52,24 +53,32 @@ reconfigure=false runUnattended=false # Compatibility - +distro_check() { if command -v apt-get &> /dev/null; then #Debian Family ############################################# PKG_MANAGER="apt-get" UPDATE_PKG_CACHE="${PKG_MANAGER} update" - PKG_INSTALL="${PKG_MANAGER} --yes --no-install-recommends install" + PKG_INSTALL=(${PKG_MANAGER} --yes --no-install-recommends install) # grep -c will return 1 retVal on 0 matches, block this throwing the set -e with an OR TRUE PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" # ######################################### # fixes for dependancy differences # Debian 7 doesn't have iproute2 use iproute - ${PKG_MANAGER} install --dry-run iproute2 > /dev/null 2>&1 && IPROUTE_PKG="iproute2" || IPROUTE_PKG="iproute" + if ${PKG_MANAGER} install --dry-run iproute2 > /dev/null 2>&1; then + iproute_pkg="iproute2" + else + iproute_pkg="iproute" + fi # Prefer the php metapackage if it's there, fall back on the php5 pacakges - ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1 && phpVer="php" || phpVer="php5" + if ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1; then + phpVer="php" + else + phpVer="php5" + fi # ######################################### INSTALLER_DEPS=(apt-utils debconf dhcpcd5 git whiptail) - PIHOLE_DEPS=(bc cron curl dnsmasq dnsutils ${IPROUTE_PKG} iputils-ping lighttpd lsof netcat ${phpVer}-common ${phpVer}-cgi sudo unzip wget) + PIHOLE_DEPS=(bc cron curl dnsmasq dnsutils ${iproute_pkg} iputils-ping lighttpd lsof netcat ${phpVer}-common ${phpVer}-cgi sudo unzip wget) LIGHTTPD_USER="www-data" LIGHTTPD_GROUP="www-data" LIGHTTPD_CFG="lighttpd.conf.debian" @@ -85,7 +94,7 @@ elif command -v rpm &> /dev/null; then # Fedora and family update cache on every PKG_INSTALL call, no need for a separate update. UPDATE_PKG_CACHE=":" - PKG_INSTALL="${PKG_MANAGER} install -y" + PKG_INSTALL=(${PKG_MANAGER} install -y) PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l" INSTALLER_DEPS=(git iproute net-tools newt procps-ng) PIHOLE_DEPS=(bc bind-utils cronie curl dnsmasq findutils lighttpd lighttpd-fastcgi nmap-ncat php php-common php-cli sudo unzip wget) @@ -102,8 +111,8 @@ else echo "OS distribution not supported" exit fi +} -####### FUNCTIONS ########## is_repo() { # Use git to check if directory is currently under VCS, return the value 128 # if directory is not a repo. Return 1 if directory does not exist. @@ -168,15 +177,19 @@ getGitFiles() { } find_IPv4_information() { + local route # Find IP used to route to outside world - IPv4dev=$(ip route get 8.8.8.8 | awk '{for(i=1;i<=NF;i++)if($i~/dev/)print $(i+1)}') - IPV4_ADDRESS=$(ip route get 8.8.8.8| awk '{print $7}') - IPv4gw=$(ip route get 8.8.8.8 | awk '{print $3}') + route=$(ip route get 8.8.8.8) + IPv4dev=$(awk '{for (i=1; i<=NF; i++) if ($i~/dev/) print $(i+1)}' <<< "${route}") + IPv4bare=$(awk '{print $7}' <<< "${route}") + IPV4_ADDRESS=$(ip -o -f inet addr show | grep "${IPv4bare}" | awk '{print $4}' | awk 'END {print}') + IPv4gw=$(awk '{print $3}' <<< "${route}") + } get_available_interfaces() { # Get available UP interfaces. - availableInterfaces=$(ip -o link | grep "state UP" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1) + availableInterfaces=$(ip -o link | grep -v "state DOWN\|lo" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1) } welcomeDialogs() { @@ -235,32 +248,28 @@ chooseInterface() { # Loop sentinel variable local firstLoop=1 - if [[ $(echo "${availableInterfaces}" | wc -l) -eq 1 ]]; then - PIHOLE_INTERFACE="${availableInterfaces}" - return - fi - - while read -r line; do - mode="OFF" - if [[ ${firstLoop} -eq 1 ]]; then - firstLoop=0 - mode="ON" - fi - interfacesArray+=("${line}" "available" "${mode}") - done <<< "${availableInterfaces}" - # Find out how many interfaces are available to choose from interfaceCount=$(echo "${availableInterfaces}" | wc -l) - chooseInterfaceCmd=(whiptail --separate-output --radiolist "Choose An Interface (press space to select)" ${r} ${c} ${interfaceCount}) - chooseInterfaceOptions=$("${chooseInterfaceCmd[@]}" "${interfacesArray[@]}" 2>&1 >/dev/tty) - if [[ $? = 0 ]]; then - for desiredInterface in ${chooseInterfaceOptions}; do - PIHOLE_INTERFACE=${desiredInterface} - echo "::: Using interface: $PIHOLE_INTERFACE" - done + + if [[ ${interfaceCount} -eq 1 ]]; then + PIHOLE_INTERFACE="${availableInterfaces}" else - echo "::: Cancel selected, exiting...." - exit 1 + while read -r line; do + mode="OFF" + if [[ ${firstLoop} -eq 1 ]]; then + firstLoop=0 + mode="ON" + fi + interfacesArray+=("${line}" "available" "${mode}") + done <<< "${availableInterfaces}" + + chooseInterfaceCmd=(whiptail --separate-output --radiolist "Choose An Interface (press space to select)" ${r} ${c} ${interfaceCount}) + chooseInterfaceOptions=$("${chooseInterfaceCmd[@]}" "${interfacesArray[@]}" 2>&1 >/dev/tty) || \ + { echo "::: Cancel selected. Exiting"; exit 1; } + for desiredInterface in ${chooseInterfaceOptions}; do + PIHOLE_INTERFACE=${desiredInterface} + echo "::: Using interface: $PIHOLE_INTERFACE" + done fi } @@ -281,41 +290,37 @@ use4andor6() { cmd=(whiptail --separate-output --checklist "Select Protocols (press space to select)" ${r} ${c} 2) options=(IPv4 "Block ads over IPv4" on IPv6 "Block ads over IPv6" on) - choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) - if [[ $? = 0 ]];then - for choice in ${choices} - do - case ${choice} in - IPv4 ) useIPv4=true;; - IPv6 ) useIPv6=true;; - esac - done - if [[ ${useIPv4} ]]; then - find_IPv4_information - getStaticIPv4Settings - setStaticIPv4 - fi - if [[ ${useIPv6} ]]; then - useIPv6dialog - fi - echo "::: IPv4 address: ${IPV4_ADDRESS}" - echo "::: IPv6 address: ${IPV6_ADDRESS}" - if [ ! ${useIPv4} ] && [ ! ${useIPv6} ]; then - echo "::: Cannot continue, neither IPv4 or IPv6 selected" - echo "::: Exiting" - exit 1 - fi - else - echo "::: Cancel selected. Exiting..." + choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) || { echo "::: Cancel selected. Exiting"; exit 1; } + for choice in ${choices} + do + case ${choice} in + IPv4 ) useIPv4=true;; + IPv6 ) useIPv6=true;; + esac + done + if [[ ${useIPv4} ]]; then + find_IPv4_information + getStaticIPv4Settings + setStaticIPv4 + fi + if [[ ${useIPv6} ]]; then + useIPv6dialog + fi + echo "::: IPv4 address: ${IPV4_ADDRESS}" + echo "::: IPv6 address: ${IPV6_ADDRESS}" + if [ ! ${useIPv4} ] && [ ! ${useIPv6} ]; then + echo "::: Cannot continue, neither IPv4 or IPv6 selected" + echo "::: Exiting" exit 1 fi } getStaticIPv4Settings() { + local ipSettingsCorrect # Ask if the user wants to use DHCP settings as their static IP - if (whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Do you want to use your current network settings as a static address? + if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Do you want to use your current network settings as a static address? IP address: ${IPV4_ADDRESS} - Gateway: ${IPv4gw}" ${r} ${c}); then + Gateway: ${IPv4gw}" ${r} ${c}; then # If they choose yes, let the user know that the IP address will not be available via DHCP and may cause a conflict. whiptail --msgbox --backtitle "IP information" --title "FYI: IP Conflict" "It is possible your router could still try to assign this IP to a device, which would cause a conflict. But in most cases the router is smart enough to not do that. If you are worried, either manually set the address, or modify the DHCP reservation pool so it does not include the IP you want. @@ -326,36 +331,29 @@ It is also possible to use a DHCP reservation, but if you are going to do that, # Start by getting the IPv4 address (pre-filling it with info gathered from DHCP) # Start a loop to let the user enter their information with the chance to go back and edit it if necessary until [[ ${ipSettingsCorrect} = True ]]; do + # Ask for the IPv4 address - IPV4_ADDRESS=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 address" --inputbox "Enter your desired IPv4 address" ${r} ${c} "${IPV4_ADDRESS}" 3>&1 1>&2 2>&3) - if [[ $? = 0 ]]; then + IPV4_ADDRESS=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 address" --inputbox "Enter your desired IPv4 address" ${r} ${c} "${IPV4_ADDRESS}" 3>&1 1>&2 2>&3) || \ + # Cancelling IPv4 settings window + { ipSettingsCorrect=False; echo "::: Cancel selected. Exiting..."; exit 1; } echo "::: Your static IPv4 address: ${IPV4_ADDRESS}" + # Ask for the gateway - IPv4gw=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 gateway (router)" --inputbox "Enter your desired IPv4 default gateway" ${r} ${c} "${IPv4gw}" 3>&1 1>&2 2>&3) - if [[ $? = 0 ]]; then - echo "::: Your static IPv4 gateway: ${IPv4gw}" - # Give the user a chance to review their settings before moving on - if (whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Are these settings correct? - IP address: ${IPV4_ADDRESS} - Gateway: ${IPv4gw}" ${r} ${c}); then - # After that's done, the loop ends and we move on - ipSettingsCorrect=True + IPv4gw=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 gateway (router)" --inputbox "Enter your desired IPv4 default gateway" ${r} ${c} "${IPv4gw}" 3>&1 1>&2 2>&3) || \ + # Cancelling gateway settings window + { ipSettingsCorrect=False; echo "::: Cancel selected. Exiting..."; exit 1; } + echo "::: Your static IPv4 gateway: ${IPv4gw}" + + # Give the user a chance to review their settings before moving on + if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Are these settings correct? + IP address: ${IPV4_ADDRESS} + Gateway: ${IPv4gw}" ${r} ${c}; then + # After that's done, the loop ends and we move on + ipSettingsCorrect=True else - # If the settings are wrong, the loop continues - ipSettingsCorrect=False - fi - else - # Cancelling gateway settings window + # If the settings are wrong, the loop continues ipSettingsCorrect=False - echo "::: Cancel selected. Exiting..." - exit 1 fi - else - # Cancelling IPv4 settings window - ipSettingsCorrect=False - echo "::: Cancel selected. Exiting..." - exit 1 - fi done # End the if statement for DHCP vs. static fi @@ -439,95 +437,88 @@ valid_ip() { } setDNS() { - DNSChooseCmd=(whiptail --separate-output --radiolist "Select Upstream DNS Provider. To use your own, select Custom." ${r} ${c} 6) - DNSChooseOptions=(Google "" on - OpenDNS "" off - Level3 "" off - Norton "" off - Comodo "" off - Custom "" off) - DNSchoices=$("${DNSChooseCmd[@]}" "${DNSChooseOptions[@]}" 2>&1 >/dev/tty) - if [[ $? = 0 ]];then - case ${DNSchoices} in - Google) - echo "::: Using Google DNS servers." - PIHOLE_DNS_1="8.8.8.8" - PIHOLE_DNS_2="8.8.4.4" - ;; - OpenDNS) - echo "::: Using OpenDNS servers." - PIHOLE_DNS_1="208.67.222.222" - PIHOLE_DNS_2="208.67.220.220" - ;; - Level3) - echo "::: Using Level3 servers." - PIHOLE_DNS_1="4.2.2.1" - PIHOLE_DNS_2="4.2.2.2" - ;; - Norton) - echo "::: Using Norton ConnectSafe servers." - PIHOLE_DNS_1="199.85.126.10" - PIHOLE_DNS_2="199.85.127.10" - ;; - Comodo) - echo "::: Using Comodo Secure servers." - PIHOLE_DNS_1="8.26.56.26" - PIHOLE_DNS_2="8.20.247.20" - ;; - Custom) - until [[ ${DNSSettingsCorrect} = True ]]; do - strInvalid="Invalid" - if [ ! ${PIHOLE_DNS_1} ]; then - if [ ! ${PIHOLE_DNS_2} ]; then - prePopulate="" - else - prePopulate=", ${PIHOLE_DNS_2}" - fi - elif [ ${PIHOLE_DNS_1} ] && [ ! ${PIHOLE_DNS_2} ]; then - prePopulate="${PIHOLE_DNS_1}" - elif [ ${PIHOLE_DNS_1} ] && [ ${PIHOLE_DNS_2} ]; then - prePopulate="${PIHOLE_DNS_1}, ${PIHOLE_DNS_2}" + local DNSSettingsCorrect + + DNSChooseOptions=(Google "" + OpenDNS "" + Level3 "" + Norton "" + Comodo "" + Custom "") + DNSchoices=$(whiptail --separate-output --menu "Select Upstream DNS Provider. To use your own, select Custom." ${r} ${c} 6 \ + "${DNSChooseOptions[@]}" 2>&1 >/dev/tty) || \ + { echo "::: Cancel selected. Exiting"; exit 1; } + case ${DNSchoices} in + Google) + echo "::: Using Google DNS servers." + PIHOLE_DNS_1="8.8.8.8" + PIHOLE_DNS_2="8.8.4.4" + ;; + OpenDNS) + echo "::: Using OpenDNS servers." + PIHOLE_DNS_1="208.67.222.222" + PIHOLE_DNS_2="208.67.220.220" + ;; + Level3) + echo "::: Using Level3 servers." + PIHOLE_DNS_1="4.2.2.1" + PIHOLE_DNS_2="4.2.2.2" + ;; + Norton) + echo "::: Using Norton ConnectSafe servers." + PIHOLE_DNS_1="199.85.126.10" + PIHOLE_DNS_2="199.85.127.10" + ;; + Comodo) + echo "::: Using Comodo Secure servers." + PIHOLE_DNS_1="8.26.56.26" + PIHOLE_DNS_2="8.20.247.20" + ;; + Custom) + until [[ ${DNSSettingsCorrect} = True ]]; do + strInvalid="Invalid" + if [ ! ${PIHOLE_DNS_1} ]; then + if [ ! ${PIHOLE_DNS_2} ]; then + prePopulate="" + else + prePopulate=", ${PIHOLE_DNS_2}" fi + elif [ ${PIHOLE_DNS_1} ] && [ ! ${PIHOLE_DNS_2} ]; then + prePopulate="${PIHOLE_DNS_1}" + elif [ ${PIHOLE_DNS_1} ] && [ ${PIHOLE_DNS_2} ]; then + prePopulate="${PIHOLE_DNS_1}, ${PIHOLE_DNS_2}" + fi - piholeDNS=$(whiptail --backtitle "Specify Upstream DNS Provider(s)" --inputbox "Enter your desired upstream DNS provider(s), seperated by a comma.\n\nFor example '8.8.8.8, 8.8.4.4'" ${r} ${c} "${prePopulate}" 3>&1 1>&2 2>&3) - - if [[ $? = 0 ]]; then - PIHOLE_DNS_1=$(echo "${piholeDNS}" | sed 's/[, \t]\+/,/g' | awk -F, '{print$1}') - PIHOLE_DNS_2=$(echo "${piholeDNS}" | sed 's/[, \t]\+/,/g' | awk -F, '{print$2}') - if ! valid_ip "${PIHOLE_DNS_1}" || [ ! "${PIHOLE_DNS_1}" ]; then - PIHOLE_DNS_1=${strInvalid} - fi - if ! valid_ip "${PIHOLE_DNS_2}" && [ "${PIHOLE_DNS_2}" ]; then - PIHOLE_DNS_2=${strInvalid} - fi - else - echo "::: Cancel selected, exiting...." - exit 1 + piholeDNS=$(whiptail --backtitle "Specify Upstream DNS Provider(s)" --inputbox "Enter your desired upstream DNS provider(s), seperated by a comma.\n\nFor example '8.8.8.8, 8.8.4.4'" ${r} ${c} "${prePopulate}" 3>&1 1>&2 2>&3) || \ + { echo "::: Cancel selected. Exiting"; exit 1; } + PIHOLE_DNS_1=$(echo "${piholeDNS}" | sed 's/[, \t]\+/,/g' | awk -F, '{print$1}') + PIHOLE_DNS_2=$(echo "${piholeDNS}" | sed 's/[, \t]\+/,/g' | awk -F, '{print$2}') + if ! valid_ip "${PIHOLE_DNS_1}" || [ ! "${PIHOLE_DNS_1}" ]; then + PIHOLE_DNS_1=${strInvalid} + fi + if ! valid_ip "${PIHOLE_DNS_2}" && [ "${PIHOLE_DNS_2}" ]; then + PIHOLE_DNS_2=${strInvalid} + fi + if [[ ${PIHOLE_DNS_1} == "${strInvalid}" ]] || [[ ${PIHOLE_DNS_2} == "${strInvalid}" ]]; then + whiptail --msgbox --backtitle "Invalid IP" --title "Invalid IP" "One or both entered IP addresses were invalid. Please try again.\n\n DNS Server 1: $PIHOLE_DNS_1\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c} + if [[ ${PIHOLE_DNS_1} == "${strInvalid}" ]]; then + PIHOLE_DNS_1="" fi - if [[ ${PIHOLE_DNS_1} == "${strInvalid}" ]] || [[ ${PIHOLE_DNS_2} == "${strInvalid}" ]]; then - whiptail --msgbox --backtitle "Invalid IP" --title "Invalid IP" "One or both entered IP addresses were invalid. Please try again.\n\n DNS Server 1: $PIHOLE_DNS_1\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c} - if [[ ${PIHOLE_DNS_1} == "${strInvalid}" ]]; then - PIHOLE_DNS_1="" - fi - if [[ ${PIHOLE_DNS_2} == "${strInvalid}" ]]; then - PIHOLE_DNS_2="" - fi - DNSSettingsCorrect=False - else - if (whiptail --backtitle "Specify Upstream DNS Provider(s)" --title "Upstream DNS Provider(s)" --yesno "Are these settings correct?\n DNS Server 1: $PIHOLE_DNS_1\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c}); then - DNSSettingsCorrect=True - else - # If the settings are wrong, the loop continues - DNSSettingsCorrect=False - fi + if [[ ${PIHOLE_DNS_2} == "${strInvalid}" ]]; then + PIHOLE_DNS_2="" fi - done - ;; - esac - else - echo "::: Cancel selected. Exiting..." - exit 1 - fi + DNSSettingsCorrect=False + else + if (whiptail --backtitle "Specify Upstream DNS Provider(s)" --title "Upstream DNS Provider(s)" --yesno "Are these settings correct?\n DNS Server 1: $PIHOLE_DNS_1\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c}); then + DNSSettingsCorrect=True + else + # If the settings are wrong, the loop continues + DNSSettingsCorrect=False + fi + fi + done + ;; + esac } setLogging() { @@ -610,35 +601,35 @@ clean_existing() { # Clean an exiting installation to prepare for upgrade/reinstall # ${1} Directory to clean; ${2} Array of files to remove local clean_directory="${1}" - local old_files=${2} + shift + local old_files=( "$@" ) for script in "${old_files[@]}"; do - rm -f "${clean_directory}${script}.sh" + rm -f "${clean_directory}/${script}.sh" done } installScripts() { # Install the scripts from repository to their various locations - readonly install_dir="/opt/pihole/" echo ":::" echo -n "::: Installing scripts from ${PI_HOLE_LOCAL_REPO}..." # Clear out script files from Pi-hole scripts directory. - clean_existing "${install_dir}" "${PI_HOLE_FILES}" + clean_existing "${PI_HOLE_INSTALL_DIR}" "${PI_HOLE_FILES[@]}" # Install files from local core repository if is_repo "${PI_HOLE_LOCAL_REPO}"; then cd "${PI_HOLE_LOCAL_REPO}" - install -o "${USER}" -Dm755 -d /opt/pihole - install -o "${USER}" -Dm755 -t /opt/pihole/ gravity.sh - install -o "${USER}" -Dm755 -t /opt/pihole/ ./advanced/Scripts/*.sh - install -o "${USER}" -Dm755 -t /opt/pihole/ ./automated\ install/uninstall.sh + install -o "${USER}" -Dm755 -d "${PI_HOLE_INSTALL_DIR}" + install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" gravity.sh + install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./advanced/Scripts/*.sh + install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./automated\ install/uninstall.sh install -o "${USER}" -Dm755 -t /usr/local/bin/ pihole install -Dm644 ./advanced/bash-completion/pihole /etc/bash_completion.d/pihole echo " done." else - echo " *** ERROR: Local repo ${core_repo} not found, exiting." + echo " *** ERROR: Local repo ${PI_HOLE_LOCAL_REPO} not found, exiting." exit 1 fi } @@ -757,7 +748,7 @@ install_dependent_packages() { fi done if [[ ${#installArray[@]} -gt 0 ]]; then - debconf-apt-progress -- ${PKG_INSTALL} "${installArray[@]}" + debconf-apt-progress -- "${PKG_INSTALL[@]}" "${installArray[@]}" return fi return 0 @@ -774,7 +765,7 @@ install_dependent_packages() { fi done if [[ ${#installArray[@]} -gt 0 ]]; then - ${PKG_INSTALL} "${installArray[@]}" &> /dev/null + "${PKG_INSTALL[@]}" "${installArray[@]}" &> /dev/null return fi return 0 @@ -818,21 +809,23 @@ installPiholeWeb() { if [ -f "/var/www/html/pihole/blockingpage.css" ]; then echo "::: Existing blockingpage.css detected, not overwriting" else - echo -n "::: index.css missing, replacing... " + echo -n "::: blockingpage.css missing, replacing... " cp /etc/.pihole/advanced/blockingpage.css /var/www/html/pihole echo " done!" fi else - mkdir /var/www/html/pihole + echo "::: Creating directory for blocking page" + install -d /var/www/html/pihole + install -D /etc/.pihole/advanced/{index,blockingpage}.* /var/www/html/pihole/ if [ -f /var/www/html/index.lighttpd.html ]; then mv /var/www/html/index.lighttpd.html /var/www/html/index.lighttpd.orig else printf "\n:::\tNo default index.lighttpd.html file found... not backing up" fi - cp /etc/.pihole/advanced/index.* /var/www/html/pihole/. echo " done!" fi + # Install Sudoer file echo ":::" echo -n "::: Installing sudoer file..." @@ -878,29 +871,42 @@ runGravity() { create_pihole_user() { # Check if user pihole exists and create if not echo "::: Checking if user 'pihole' exists..." - id -u pihole &> /dev/null && echo "::: User 'pihole' already exists" || (echo "::: User 'pihole' doesn't exist. Creating..." && useradd -r -s /usr/sbin/nologin pihole) + if id -u pihole &> /dev/null; then + echo "::: User 'pihole' already exists" + else + echo "::: User 'pihole' doesn't exist. Creating..." + useradd -r -s /usr/sbin/nologin pihole + fi } configureFirewall() { # Allow HTTP and DNS traffic if firewall-cmd --state &> /dev/null; then - echo "::: Configuring FirewallD for httpd and dnsmasq.." + whiptail --title "Firewall in use" --yesno "We have detected a running firewall\n\nPi-hole currently requires HTTP and DNS port access.\n\n\n\nInstall Pi-hole default firewall rules?" ${r} ${c} || \ + { echo -e ":::\n::: Not installing firewall rulesets."; return 0; } + echo -e ":::\n:::\n Configuring FirewallD for httpd and dnsmasq." firewall-cmd --permanent --add-port=80/tcp --add-port=53/tcp --add-port=53/udp firewall-cmd --reload + return 0 # Check for proper kernel modules to prevent failure - elif modinfo ip_tables &> /dev/null; then + elif modinfo ip_tables &> /dev/null && command -v iptables &> /dev/null; then # If chain Policy is not ACCEPT or last Rule is not ACCEPT # then check and insert our Rules above the DROP/REJECT Rule. if iptables -S INPUT | head -n1 | grep -qv '^-P.*ACCEPT$' || iptables -S INPUT | tail -n1 | grep -qv '^-\(A\|P\).*ACCEPT$'; then + whiptail --title "Firewall in use" --yesno "We have detected a running firewall\n\nPi-hole currently requires HTTP and DNS port access.\n\n\n\nInstall Pi-hole default firewall rules?" ${r} ${c} || \ + { echo -e ":::\n::: Not installing firewall rulesets."; return 0; } + echo -e ":::\n::: Installing new IPTables firewall rulesets." # Check chain first, otherwise a new rule will duplicate old ones - echo "::: Configuring iptables for httpd and dnsmasq.." iptables -C INPUT -p tcp -m tcp --dport 80 -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p tcp -m tcp --dport 80 -j ACCEPT iptables -C INPUT -p tcp -m tcp --dport 53 -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT iptables -C INPUT -p udp -m udp --dport 53 -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT + return 0 fi else - echo "::: No active firewall detected.. skipping firewall configuration." + echo -e ":::\n::: No active firewall detected.. skipping firewall configuration." + return 0 fi + echo -e ":::\n::: Skipping firewall configuration." } finalExports() { @@ -930,6 +936,24 @@ finalExports() { fi } +installLogrotate() { + # Install the logrotate script + echo ":::" + echo -n "::: Installing latest logrotate script..." + cp /etc/.pihole/advanced/logrotate /etc/pihole/logrotate + # Different operating systems have different user / group + # settings for logrotate that makes it impossible to create + # a static logrotate file that will work with e.g. + # Rasbian and Ubuntu at the same time. Hence, we have to + # customize the logrotate script here in order to reflect + # the local properties of the /var/log directory + logusergroup="$(stat -c '%U %G' /var/log)" + if [[ ! -z $logusergroup ]]; then + echo "su ${logusergroup}" >> /etc/pihole/logrotate + fi + echo " done!" +} + installPihole() { # Install base files and web interface create_pihole_user @@ -949,6 +973,7 @@ installPihole() { CreateLogFile installPiholeWeb installCron + installLogrotate configureFirewall finalExports runGravity @@ -972,14 +997,14 @@ accountForRefactor() { updatePihole() { accountForRefactor # Source ${setupVars} for use in the rest of the functions. - . ${setupVars} + source ${setupVars} # Install base files and web interface installScripts installConfigs CreateLogFile installPiholeWeb installCron - configureFirewall + installLogrotate finalExports #re-export setupVars.conf to account for any new vars added in new versions runGravity } @@ -993,15 +1018,11 @@ checkSelinux() { enforceMode=$(getenforce) echo "${enforceMode}" if [[ "${enforceMode}" == "Enforcing" ]]; then - if (whiptail --title "SELinux Enforcing Detected" --yesno "SELinux is being Enforced on your system!\n\nPi-hole currently does not support SELinux, but you may still continue with the installation.\n\nNote: Admin UI Will not function fully without setting your policies correctly\n\nContinue installing Pi-hole?" ${r} ${c}); then - echo ":::" - echo "::: Continuing installation with SELinux Enforcing." - echo "::: Please refer to official SELinux documentation to create a custom policy." - else - echo ":::" - echo "::: Not continuing install after SELinux Enforcing detected." - exit 1 - fi + whiptail --title "SELinux Enforcing Detected" --yesno "SELinux is being Enforced on your system!\n\nPi-hole currently does not support SELinux, but you may still continue with the installation.\n\nNote: Admin UI Will not function fully without setting your policies correctly\n\nContinue installing Pi-hole?" ${r} ${c} || \ + { echo ":::"; echo "::: Not continuing install after SELinux Enforcing detected."; exit 1; } + echo ":::" + echo "::: Continuing installation with SELinux Enforcing." + echo "::: Please refer to official SELinux documentation to create a custom policy." fi fi } @@ -1037,23 +1058,19 @@ update_dialogs() { UpdateCmd=$(whiptail --title "Existing Install Detected!" --menu "\n\nWe have detected an existing install.\n\nPlease choose from the following options: \n($strAdd)" ${r} ${c} 2 \ "${opt1a}" "${opt1b}" \ - "${opt2a}" "${opt2b}" 3>&2 2>&1 1>&3) + "${opt2a}" "${opt2b}" 3>&2 2>&1 1>&3) || \ + { echo "::: Cancel selected. Exiting"; exit 1; } - if [[ $? = 0 ]];then - case ${UpdateCmd} in - ${opt1a}) - echo "::: ${opt1a} option selected." - useUpdateVars=true - ;; - ${opt2a}) - echo "::: ${opt2a} option selected" - useUpdateVars=false - ;; + case ${UpdateCmd} in + ${opt1a}) + echo "::: ${opt1a} option selected." + useUpdateVars=true + ;; + ${opt2a}) + echo "::: ${opt2a} option selected" + useUpdateVars=false + ;; esac - else - echo "::: Cancel selected. Exiting..." - exit 1 - fi } main() { @@ -1072,7 +1089,7 @@ main() { if command -v sudo &> /dev/null; then echo "::: Utility sudo located." - exec curl -sSL https://install.pi-hole.net | sudo bash "$@" + exec curl -sSL https://raw.githubusercontent.com/pi-hole/pi-hole/master/automated%20install/basic-install.sh | sudo bash "$@" exit $? else echo "::: sudo is needed for the Web interface to run pihole commands. Please run this script as root and it will be automatically installed." @@ -1080,6 +1097,9 @@ main() { fi fi + # Check for supported distribution + distro_check + # Check arguments for the undocumented flags for var in "$@"; do case "$var" in @@ -1170,11 +1190,11 @@ main() { pw="" if [[ $(grep 'WEBPASSWORD' -c /etc/pihole/setupVars.conf) == 0 ]] ; then pw=$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c 8) - pihole -a -p ${pw} + /usr/local/bin/pihole -a -p "${pw}" fi if [[ "${useUpdateVars}" == false ]]; then - displayFinalMessage ${pw} + displayFinalMessage "${pw}" fi echo "::: Restarting services..." @@ -76,7 +76,7 @@ gravity_collapse() { #custom file found, use this instead of default echo -n "::: Custom adList file detected. Reading..." sources=() - while read -r line; do + while IFS= read -r line || [[ -n "$line" ]]; do #Do not read commented out or blank lines if [[ ${line} = \#* ]] || [[ ! ${line} ]]; then echo "" > /dev/null @@ -89,7 +89,7 @@ gravity_collapse() { #no custom file found, use defaults! echo -n "::: No custom adlist file detected, reading from default file..." sources=() - while read -r line; do + while IFS= read -r line || [[ -n "$line" ]]; do #Do not read commented out or blank lines if [[ ${line} = \#* ]] || [[ ! ${line} ]]; then echo "" > /dev/null @@ -388,7 +388,7 @@ if [[ "${forceGrav}" == true ]]; then fi #Overwrite adlists.default from /etc/.pihole in case any changes have been made. Changes should be saved in /etc/adlists.list -#cp /etc/.pihole/adlists.default /etc/pihole/adlists.default +cp /etc/.pihole/adlists.default /etc/pihole/adlists.default gravity_collapse gravity_spinup if [[ "${skipDownload}" == false ]]; then @@ -11,6 +11,7 @@ # (at your option) any later version. PI_HOLE_SCRIPT_DIR="/opt/pihole" +readonly wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf" # Must be root to use this tool if [[ ! $EUID -eq 0 ]];then if [ -x "$(command -v sudo)" ];then @@ -38,6 +39,11 @@ blacklistFunc() { exit 0 } +wildcardFunc() { + "${PI_HOLE_SCRIPT_DIR}"/list.sh "$@" + exit 0 +} + debugFunc() { "${PI_HOLE_SCRIPT_DIR}"/piholeDebug.sh exit 0 @@ -63,11 +69,6 @@ updateGravityFunc() { exit 0 } -setupLCDFunction() { - "${PI_HOLE_SCRIPT_DIR}"/setupLCD.sh - exit 0 -} - scanList(){ domain="${1}" list="${2}" @@ -79,19 +80,52 @@ scanList(){ fi } +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() { domain="${2}" method="${3}" lists=( /etc/pihole/list.* /etc/pihole/blacklist.txt) for list in ${lists[@]}; do - result=$(scanList ${domain} ${list} ${method}) + if [ -e "${list}" ]; then + result=$(scanList ${domain} ${list} ${method}) + # Remove empty lines before couting number of results + count=$(sed '/^\s*$/d' <<< "$result" | wc -l) + echo "::: ${list} (${count} results)" + if [[ ${count} > 0 ]]; then + echo "${result}" + fi + echo "" + else + echo "::: ${list} does not exist" + echo "" + fi + done + + # Scan for possible wildcard matches + local wildcards=($(processWildcards "${domain}")) + for domain in ${wildcards[@]}; do + result=$(scanList "\/${domain}\/" ${wildcardlist}) # Remove empty lines before couting number of results count=$(sed '/^\s*$/d' <<< "$result" | wc -l) - echo "::: ${list} (${count} results)" if [[ ${count} > 0 ]]; then + echo "::: Wildcard blocking ${domain} (${count} results)" echo "${result}" + echo "" fi - echo "" done exit 0 } @@ -248,7 +282,6 @@ helpFunc() { ::: -up, updatePihole Update Pi-hole ::: -r, reconfigure Reconfigure or Repair Pi-hole ::: -g, updateGravity Update the list of ad-serving domains -::: -s, setupLCD Automatically configures the Pi to use the 2.8 LCD screen to display stats on it ::: -c, chronometer Calculates stats and displays to an LCD ::: -h, help Show this help dialog ::: -v, version Show current versions @@ -275,12 +308,12 @@ fi case "${1}" in "-w" | "whitelist" ) whitelistFunc "$@";; "-b" | "blacklist" ) blacklistFunc "$@";; + "-wild" | "wildcard" ) wildcardFunc "$@";; "-d" | "debug" ) debugFunc;; "-f" | "flush" ) flushFunc;; "-up" | "updatePihole" ) updatePiholeFunc;; "-r" | "reconfigure" ) reconfigurePiholeFunc;; "-g" | "updateGravity" ) updateGravityFunc "$@";; - "-s" | "setupLCD" ) setupLCDFunction;; "-c" | "chronometer" ) chronometerFunc "$@";; "-h" | "help" ) helpFunc;; "-v" | "version" ) versionFunc "$@";; diff --git a/test/test_automated_install.py b/test/test_automated_install.py index 58aefe91..f8e2ec3d 100644 --- a/test/test_automated_install.py +++ b/test/test_automated_install.py @@ -64,35 +64,237 @@ def test_setupVars_saved_to_file(Pihole): for k,v in SETUPVARS.iteritems(): assert "{}={}".format(k, v) in output -def test_configureFirewall_firewalld_no_errors(Pihole): - ''' confirms firewalld rules are applied when appopriate ''' - mock_command('firewall-cmd', '0', Pihole) +def test_configureFirewall_firewalld_running_no_errors(Pihole): + ''' 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 + mock_command('whiptail', {'*':('', 0)}, Pihole) configureFirewall = Pihole.run(''' source /opt/pihole/basic-install.sh configureFirewall ''') - expected_stdout = '::: Configuring FirewallD for httpd and dnsmasq.' + expected_stdout = 'Configuring FirewallD for httpd and dnsmasq.' 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-port=80/tcp --add-port=53/tcp --add-port=53/udp' 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 ''' + # 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.' + 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 ''' + # firewallD returns running status + mock_command('firewall-cmd', {'*':('running', 0)}, Pihole) + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*':('', 1)}, Pihole) + configureFirewall = Pihole.run(''' + source /opt/pihole/basic-install.sh + configureFirewall + ''') + 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(''' + source /opt/pihole/basic-install.sh + configureFirewall + ''') + 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 ''' + # iptables command exists + mock_command('iptables', {'*':('', '0')}, Pihole) + # modinfo returns always true (ip_tables module check) + mock_command('modinfo', {'*':('', '0')}, Pihole) + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*':('', '1')}, Pihole) + configureFirewall = Pihole.run(''' + source /opt/pihole/basic-install.sh + configureFirewall + ''') + 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) + mock_command('iptables', {'-S':('-P INPUT DENY', '0')}, Pihole) + # modinfo returns always true (ip_tables module check) + mock_command('modinfo', {'*':('', '0')}, Pihole) + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*':('', '0')}, Pihole) + configureFirewall = Pihole.run(''' + source /opt/pihole/basic-install.sh + configureFirewall + ''') + 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 + +def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole): + ''' 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) + # modinfo returns always true (ip_tables module check) + mock_command('modinfo', {'*':('', '0')}, Pihole) + # Whiptail dialog returns Cancel for user prompt + mock_command('whiptail', {'*':('', '0')}, Pihole) + configureFirewall = Pihole.run(''' + source /opt/pihole/basic-install.sh + configureFirewall + ''') + 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 + +def test_installPiholeWeb_fresh_install_no_errors(Pihole): + ''' 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 'Installing pihole custom index page...' in installWeb.stdout + assert 'No default index.lighttpd.html file found... not backing up' in installWeb.stdout + web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout + assert 'index.php' in web_directory + assert 'index.js' in web_directory + assert 'blockingpage.css' in web_directory + +def test_installPiholeWeb_empty_directory_no_errors(Pihole): + ''' confirms all web page assets from Core repo are installed in an emtpy directory ''' + installWeb = Pihole.run(''' + source /opt/pihole/basic-install.sh + mkdir -p /var/www/html/pihole + installPiholeWeb + ''') + assert 'Installing pihole custom index page...' in installWeb.stdout + assert 'No default index.lighttpd.html file found... not backing up' not in installWeb.stdout + assert 'index.php missing, replacing...' in installWeb.stdout + assert 'index.js missing, replacing...' in installWeb.stdout + assert 'blockingpage.css missing, replacing...' in installWeb.stdout + web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout + assert 'index.php' in web_directory + assert 'index.js' in web_directory + assert 'blockingpage.css' in web_directory + +def test_installPiholeWeb_index_php_no_errors(Pihole): + ''' confirms all web page assets from Core repo are installed when necessary ''' + installWeb = Pihole.run(''' + source /opt/pihole/basic-install.sh + mkdir -p /var/www/html/pihole + touch /var/www/html/pihole/index.php + installPiholeWeb + ''') + assert 'Installing pihole custom index page...' in installWeb.stdout + assert 'No default index.lighttpd.html file found... not backing up' not in installWeb.stdout + assert 'Existing index.php detected, not overwriting' in installWeb.stdout + assert 'index.js missing, replacing...' in installWeb.stdout + assert 'blockingpage.css missing, replacing...' in installWeb.stdout + web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout + assert 'index.php' in web_directory + assert 'index.js' in web_directory + assert 'blockingpage.css' in web_directory + +def test_installPiholeWeb_index_js_no_errors(Pihole): + ''' confirms all web page assets from Core repo are installed when necessary ''' + installWeb = Pihole.run(''' + source /opt/pihole/basic-install.sh + mkdir -p /var/www/html/pihole + touch /var/www/html/pihole/index.js + installPiholeWeb + ''') + assert 'Installing pihole custom index page...' in installWeb.stdout + assert 'No default index.lighttpd.html file found... not backing up' not in installWeb.stdout + assert 'index.php missing, replacing...' in installWeb.stdout + assert 'Existing index.js detected, not overwriting' in installWeb.stdout + assert 'blockingpage.css missing, replacing...' in installWeb.stdout + web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout + assert 'index.php' in web_directory + assert 'index.js' in web_directory + assert 'blockingpage.css' in web_directory + +def test_installPiholeWeb_blockingpage_css_no_errors(Pihole): + ''' confirms all web page assets from Core repo are installed when necessary ''' + installWeb = Pihole.run(''' + source /opt/pihole/basic-install.sh + mkdir -p /var/www/html/pihole + touch /var/www/html/pihole/blockingpage.css + installPiholeWeb + ''') + assert 'Installing pihole custom index page...' in installWeb.stdout + assert 'No default index.lighttpd.html file found... not backing up' not in installWeb.stdout + assert 'index.php missing, replacing...' in installWeb.stdout + assert 'index.js missing, replacing...' in installWeb.stdout + assert 'Existing blockingpage.css detected, not overwriting' in installWeb.stdout + web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout + assert 'index.php' in web_directory + assert 'index.js' in web_directory + assert 'blockingpage.css' in web_directory + +def test_installPiholeWeb_already_populated_no_errors(Pihole): + ''' confirms all web page assets from Core repo are installed when necessary ''' + installWeb = Pihole.run(''' + source /opt/pihole/basic-install.sh + mkdir -p /var/www/html/pihole + touch /var/www/html/pihole/index.php + touch /var/www/html/pihole/index.js + touch /var/www/html/pihole/blockingpage.css + installPiholeWeb + ''') + assert 'Installing pihole custom index page...' in installWeb.stdout + assert 'No default index.lighttpd.html file found... not backing up' not in installWeb.stdout + assert 'Existing index.php detected, not overwriting' in installWeb.stdout + assert 'index.php missing, replacing...' not in installWeb.stdout + assert 'Existing index.js detected, not overwriting' in installWeb.stdout + assert 'index.js missing, replacing...' not in installWeb.stdout + assert 'Existing blockingpage.css detected, not overwriting' in installWeb.stdout + assert 'blockingpage.css missing, replacing... ' not in installWeb.stdout + web_directory = Pihole.run('ls -r /var/www/html/pihole').stdout + assert 'index.php' in web_directory + assert 'index.js' in web_directory + assert 'blockingpage.css' in web_directory # Helper functions -def mock_command(script, result, container): +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 ''' - ''' TODO: support array of results that enable the results to change over multiple executions of a command ''' full_script_path = '/usr/local/bin/{}'.format(script) mock_script = dedent('''\ #!/bin/bash -e echo "\$0 \$@" >> /var/log/{script} - exit {retcode} - '''.format(script=script, retcode=result)) + 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} - '''.format(script=full_script_path, content=mock_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) |