From 42084ba88dc97c4ec299b7f395be360ce22eb468 Mon Sep 17 00:00:00 2001 From: DesktopECHO <33142753+DesktopECHO@users.noreply.github.com> Date: Sun, 9 Oct 2022 21:48:33 -0300 Subject: [PATCH] Update scripts and cosmetic fixups --- .motd | 2 +- etc/init.d/crond | 2 +- etc/init.d/dropbear | 4 +- etc/init.d/lighttpd | 2 +- etc/init.d/syslog-ng | 2 +- etc/init.d/unbound | 5 +- etc/init.d/xrdp | 2 +- etc/rc.local | 58 +- usr/local/bin/p4a-install | 2755 ++++++++++++++++++++++++++++++++++++- 9 files changed, 2812 insertions(+), 20 deletions(-) diff --git a/.motd b/.motd index 7dd5e69d..d4547231 100644 --- a/.motd +++ b/.motd @@ -1,4 +1,4 @@ -Pi-hole for Android 1.7 [20221005] +Pi-hole for Android 1.8 [20221017] Pi-hole Stats -> padd Install or Update -> p4a-install diff --git a/etc/init.d/crond b/etc/init.d/crond index 91d84cb9..4d0ad1a7 100644 --- a/etc/init.d/crond +++ b/etc/init.d/crond @@ -17,7 +17,7 @@ start() { echo exit 0 fi - echo -n "Starting crond: " + echo -n "Starting crond: " daemon --pidfile="$pidfile" $binary RETVAL=$? echo diff --git a/etc/init.d/dropbear b/etc/init.d/dropbear index b1a919e0..045f8b42 100644 --- a/etc/init.d/dropbear +++ b/etc/init.d/dropbear @@ -1,3 +1,4 @@ +# vim /etc/rc.d/init.d/dropbear #!/bin/bash # # description: dropbear ssh daemon @@ -52,7 +53,7 @@ start() { echo exit 0 fi - echo -n "Starting dropbear: " + echo -n "Starting dropbear: " daemon --pidfile="$pidfile" $dropbear -p $port -d $dsskey -r $rsakey RETVAL=$? echo @@ -115,3 +116,4 @@ genrsakey) usage ;; esac + diff --git a/etc/init.d/lighttpd b/etc/init.d/lighttpd index e850619b..63f6fd39 100644 --- a/etc/init.d/lighttpd +++ b/etc/init.d/lighttpd @@ -17,7 +17,7 @@ start() { echo exit 0 fi - echo -n "Starting lighttpd: " + echo -n "Starting lighttpd: " daemon --pidfile="$pidfile" $binary -f /etc/lighttpd/lighttpd.conf RETVAL=$? echo diff --git a/etc/init.d/syslog-ng b/etc/init.d/syslog-ng index 5b6522ad..b9b1d513 100644 --- a/etc/init.d/syslog-ng +++ b/etc/init.d/syslog-ng @@ -17,7 +17,7 @@ start() { echo exit 0 fi - echo -n "Starting syslog-ng: " + echo -n "Starting syslog-ng: " daemon --pidfile="$pidfile" $binary RETVAL=$? echo diff --git a/etc/init.d/unbound b/etc/init.d/unbound index fb71eb25..e3e15911 100644 --- a/etc/init.d/unbound +++ b/etc/init.d/unbound @@ -17,7 +17,7 @@ start() { echo exit 0 fi - echo -n "Starting unbound: " + echo -n "Starting unbound: " daemon --pidfile="$pidfile" $binary RETVAL=$? echo @@ -37,12 +37,13 @@ stop() { exit 1 fi echo -n "Stopping unbound daemon: " - pkill -HUP unbound + pkill -9 -u unbound RETVAL=$? echo if [ $RETVAL -eq 0 ]; then rm -f $lockfile $pidfile + pkill -9 -u unbound return 0 else return 1 diff --git a/etc/init.d/xrdp b/etc/init.d/xrdp index 7a425bec..5f963163 100644 --- a/etc/init.d/xrdp +++ b/etc/init.d/xrdp @@ -19,7 +19,7 @@ start() { echo exit 0 fi - echo -n "Starting xrdp: " + echo -n "Starting xrdp: " /sbin/xrdp-sesman daemon --pidfile="$pidfile" $binary RETVAL=$? diff --git a/etc/rc.local b/etc/rc.local index 5675c047..f17bc958 100644 --- a/etc/rc.local +++ b/etc/rc.local @@ -1,10 +1,28 @@ -#!/bin/bash +#!/bin/sh +### BEGIN INIT INFO +# Provides: rc.local +# Required-Start: +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start daemon at boot time +# Description: Start P4A +### END INIT INFO -# Clean-up environment for startup, update PADD, install unbound for new deployment +case "$1" in +start) + +# Clean-up environment before startup, update PADD, install unbound for new deployment ntpd -gq -curl --silent --output /bin/padd https://raw.githubusercontent.com/pi-hole/PADD/master/padd.sh +curl --silent --output /bin/padd https://raw.githubusercontent.com/pi-hole/PADD/master/padd.sh & rm -rf /sbin/service /var/lock/subsys/* /tmp/* /var/run/*.pid /sbin/poweroff /sbin/reboot /bin/pihole -/bin/cp /.service /sbin/service ; /bin/cp /.poweroff /sbin/poweroff ; /bin/cp /.reboot /sbin/reboot; /bin/cp /.service /bin/update-rc.d ; ln -s /usr/local/bin/pihole /bin/pihole ; /bin/cp /.systemctl /usr/bin/systemctl ; /bin/cp /.motd /etc/motd +/bin/cp /.service /sbin/service +/bin/cp /.poweroff /sbin/poweroff +/bin/cp /.reboot /sbin/reboot +/bin/cp /.service /bin/update-rc.d +/bin/cp /.systemctl /usr/bin/systemctl +/bin/cp /.motd /etc/motd +ln -s /usr/local/bin/pihole /bin/pihole chmod +x /usr/bin/systemctl /sbin/poweroff /sbin/reboot /bin/padd [ ! -f /sbin/unbound ] && rpm -Uvh /rpm/*.rpm && cp /.pihole.conf /etc/unbound/conf.d/pihole.conf @@ -35,10 +53,10 @@ sed -i "/IPV4_ADDRESS/c\IPV4_ADDRESS=$ipaddr/$subnetmask" /etc/pihole/setupVars. sed -i "/PIHOLE_INTERFACE/c\PIHOLE_INTERFACE=$device" /etc/pihole/setupVars.conf # Start Services -yes ' ' | sed 40q +yes ' ' | sed 50q echo '_______________________________________' echo ' ' -echo ' Pi-hole for Android ' +echo ' ** Pi-hole for Android ** ' echo ' ' service syslog-ng start service crond start @@ -46,11 +64,31 @@ service dropbear start service xrdp start service lighttpd start service unbound start - -# Quietly start FTL and get better reporting from pihole status +# Quietly start FTL and get better status info after service pihole-FTL start > /var/log/pihole.log 2>&1 pihole status echo '_______________________________________' +;; + +stop) +yes ' ' | sed 50q +echo '_______________________________________' +echo ' ' +echo ' Shutting Down... ' +echo ' ' +service unbound stop +service lighttpd stop +service xrdp stop +service dropbear stop +service crond stop +service syslog-ng stop +service pihole-FTL stop +echo '_______________________________________' +;; + -# This needs to be at the end of rc.local -touch /var/lock/subsys/local +*) +echo "Usage: $0 {start|stop}" +exit 1 +esac +exit 0 diff --git a/usr/local/bin/p4a-install b/usr/local/bin/p4a-install index 4ad2c2e0..1329c64a 100644 --- a/usr/local/bin/p4a-install +++ b/usr/local/bin/p4a-install @@ -1,4 +1,18 @@ -#!/bin/bash +#!/usr/bin/env bash +# shellcheck disable=SC1090 + +# Pi-hole: A black hole for Internet advertisements +# (c) Pi-hole (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# Installs and Updates Pi-hole +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. +# pi-hole.net/donate +# + +# P4A Setup export PIHOLE_SKIP_OS_CHECK=true touch /etc/lighttpd/external.conf mkdir -p /etc/pihole @@ -9,4 +23,2741 @@ ipaddr=$(ip route list table main | tail -n1) ; ipaddr=`echo $ipaddr | awk -F' ' printf "\nPi-hole for Android // Device: $device // IP: $ipaddr // SubNet: $subnetmask\n" sed -i "/IPV4_ADDRESS/c\IPV4_ADDRESS=$ipaddr/$subnetmask" /etc/pihole/setupVars.conf sed -i "/PIHOLE_INTERFACE/c\PIHOLE_INTERFACE=$device" /etc/pihole/setupVars.conf -curl -sSL https://raw.githubusercontent.com/DesktopECHO/pi-hole/master/automated%20install/basic-install.sh | bash /dev/stdin --unattended + +# -e option instructs bash to immediately exit if any command [1] has a non-zero exit status +# We do not want users to end up with a partially working install, so we exit the script +# instead of continuing the installation with something broken +set -e + +# Append common folders to the PATH to ensure that all basic commands are available. +# When using "su" an incomplete PATH could be passed: https://github.com/pi-hole/pi-hole/issues/3209 +export PATH+=':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + +######## VARIABLES ######### +# For better maintainability, we store as much information that can change in variables +# This allows us to make a change in one place that can propagate to all instances of the variable +# These variables should all be GLOBAL variables, written in CAPS +# Local variables will be in lowercase and will exist only within functions +# It's still a work in progress, so you may see some variance in this guideline until it is complete + +# Dialog result codes +# dialog code values can be set by environment variables, we only override if +# the env var is not set or empty. +: "${DIALOG_OK:=0}" +: "${DIALOG_CANCEL:=1}" +: "${DIALOG_ESC:=255}" + + +# List of supported DNS servers +DNS_SERVERS=$(cat << EOM +Google (ECS, DNSSEC);8.8.8.8;8.8.4.4;2001:4860:4860:0:0:0:0:8888;2001:4860:4860:0:0:0:0:8844 +OpenDNS (ECS, DNSSEC);208.67.222.222;208.67.220.220;2620:119:35::35;2620:119:53::53 +Level3;4.2.2.1;4.2.2.2;; +Comodo;8.26.56.26;8.20.247.20;; +DNS.WATCH (DNSSEC);84.200.69.80;84.200.70.40;2001:1608:10:25:0:0:1c04:b12f;2001:1608:10:25:0:0:9249:d69b +Quad9 (filtered, DNSSEC);9.9.9.9;149.112.112.112;2620:fe::fe;2620:fe::9 +Quad9 (unfiltered, no DNSSEC);9.9.9.10;149.112.112.10;2620:fe::10;2620:fe::fe:10 +Quad9 (filtered, ECS, DNSSEC);9.9.9.11;149.112.112.11;2620:fe::11;2620:fe::fe:11 +Cloudflare (DNSSEC);1.1.1.1;1.0.0.1;2606:4700:4700::1111;2606:4700:4700::1001 +EOM +) + +# Location for final installation log storage +installLogLoc="/etc/pihole/install.log" +# This is an important file as it contains information specific to the machine it's being installed on +setupVars="/etc/pihole/setupVars.conf" +# Pi-hole uses lighttpd as a Web server, and this is the config file for it +lighttpdConfig="/etc/lighttpd/lighttpd.conf" +# This is a file used for the colorized output +coltable="/opt/pihole/COL_TABLE" + +# Root of the web server +webroot="/var/www/html" + + +# We clone (or update) two git repositories during the install. This helps to make sure that we always have the latest versions of the relevant files. +# AdminLTE is used to set up the Web admin interface. +# Pi-hole contains various setup scripts and files which are critical to the installation. +# Search for "PI_HOLE_LOCAL_REPO" in this file to see all such scripts. +# Two notable scripts are gravity.sh (used to generate the HOSTS file) and advanced/Scripts/webpage.sh (used to install the Web admin interface) +webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git" +webInterfaceDir="${webroot}/admin" +piholeGitUrl="https://github.com/pi-hole/pi-hole.git" +PI_HOLE_LOCAL_REPO="/etc/.pihole" +# List of pihole scripts, stored in an array +PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage) +# This directory is where the Pi-hole scripts will be installed +PI_HOLE_INSTALL_DIR="/opt/pihole" +PI_HOLE_CONFIG_DIR="/etc/pihole" +PI_HOLE_BIN_DIR="/usr/local/bin" +PI_HOLE_404_DIR="${webroot}/pihole" +if [ -z "$useUpdateVars" ]; then + useUpdateVars=false +fi + +adlistFile="/etc/pihole/adlists.list" +# Pi-hole needs an IP address; to begin, these variables are empty since we don't know what the IP is until this script can run +IPV4_ADDRESS=${IPV4_ADDRESS} +IPV6_ADDRESS=${IPV6_ADDRESS} +# Give settings their default values. These may be changed by prompts later in the script. +QUERY_LOGGING=true +INSTALL_WEB_INTERFACE=true +PRIVACY_LEVEL=0 +CACHE_SIZE=10000 + +if [ -z "${USER}" ]; then + USER="$(id -un)" +fi + +# dialog dimensions: Let dialog handle appropriate sizing. +r=20 +c=70 + +######## Undocumented Flags. Shhh ######## +# These are undocumented flags; some of which we can use when repairing an installation +# The runUnattended flag is one example of this +reconfigure=false +runUnattended=true +INSTALL_WEB_SERVER=true +# Check arguments for the undocumented flags +for var in "$@"; do + case "$var" in + "--reconfigure" ) reconfigure=true;; + "--unattended" ) runUnattended=true;; + "--disable-install-webserver" ) INSTALL_WEB_SERVER=false;; + esac +done + +# If the color table file exists, +if [[ -f "${coltable}" ]]; then + # source it + source "${coltable}" +# Otherwise, +else + # Set these values so the installer can still run in color + COL_NC='\e[0m' # No Color + COL_LIGHT_GREEN='\e[1;32m' + COL_LIGHT_RED='\e[1;31m' + TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]" + CROSS="[${COL_LIGHT_RED}✗${COL_NC}]" + INFO="[i]" + # shellcheck disable=SC2034 + DONE="${COL_LIGHT_GREEN} done!${COL_NC}" + OVER="\\r\\033[K" +fi + +# A simple function that just echoes out our logo in ASCII format +# This lets users know that it is a Pi-hole, LLC product +show_ascii_berry() { + echo -e " + ${COL_LIGHT_GREEN}.;;,. + .ccccc:,. + :cccclll:. ..,, + :ccccclll. ;ooodc + 'ccll:;ll .oooodc + .;cll.;;looo:. + ${COL_LIGHT_RED}.. ','. + .',,,,,,'. + .',,,,,,,,,,. + .',,,,,,,,,,,,.... + ....''',,,,,,,'....... + ......... .... ......... + .......... .......... + .......... .......... + ......... .... ......... + ........,,,,,,,'...... + ....',,,,,,,,,,,,. + .',,,,,,,,,'. + .',,,,,,'. + ..'''.${COL_NC} +" +} + +is_command() { + # Checks to see if the given command (passed as a string argument) exists on the system. + # The function returns 0 (success) if the command exists, and 1 if it doesn't. + local check_command="$1" + + command -v "${check_command}" >/dev/null 2>&1 +} + +os_check() { + if [ "$PIHOLE_SKIP_OS_CHECK" != true ]; then + # This function gets a list of supported OS versions from a TXT record at versions.pi-hole.net + # and determines whether or not the script is running on one of those systems + local remote_os_domain valid_os valid_version valid_response detected_os detected_version display_warning cmdResult digReturnCode response + remote_os_domain=${OS_CHECK_DOMAIN_NAME:-"versions.pi-hole.net"} + + detected_os=$(grep '^ID=' /etc/os-release | cut -d '=' -f2 | tr -d '"') + detected_version=$(grep VERSION_ID /etc/os-release | cut -d '=' -f2 | tr -d '"') + + cmdResult="$(dig +short -t txt "${remote_os_domain}" @ns1.pi-hole.net 2>&1; echo $?)" + # Gets the return code of the previous command (last line) + digReturnCode="${cmdResult##*$'\n'}" + + if [ ! "${digReturnCode}" == "0" ]; then + valid_response=false + else + # Dig returned 0 (success), so get the actual response, and loop through it to determine if the detected variables above are valid + response="${cmdResult%%$'\n'*}" + # If the value of ${response} is a single 0, then this is the return code, not an actual response. + if [ "${response}" == 0 ]; then + valid_response=false + fi + + IFS=" " read -r -a supportedOS < <(echo "${response}" | tr -d '"') + for distro_and_versions in "${supportedOS[@]}" + do + distro_part="${distro_and_versions%%=*}" + versions_part="${distro_and_versions##*=}" + + # If the distro part is a (case-insensitive) substring of the computer OS + if [[ "${detected_os^^}" =~ ${distro_part^^} ]]; then + valid_os=true + IFS="," read -r -a supportedVer <<<"${versions_part}" + for version in "${supportedVer[@]}" + do + if [[ "${detected_version}" =~ $version ]]; then + valid_version=true + break + fi + done + break + fi + done + fi + + if [ "$valid_os" = true ] && [ "$valid_version" = true ] && [ ! "$valid_response" = false ]; then + display_warning=false + fi + + if [ "$display_warning" != false ]; then + if [ "$valid_response" = false ]; then + + if [ "${digReturnCode}" -eq 0 ]; then + errStr="dig succeeded, but response was blank. Please contact support" + else + errStr="dig failed with return code ${digReturnCode}" + fi + printf " %b %bRetrieval of supported OS list failed. %s. %b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${errStr}" "${COL_NC}" + printf " %bUnable to determine if the detected OS (%s %s) is supported%b\\n" "${COL_LIGHT_RED}" "${detected_os^}" "${detected_version}" "${COL_NC}" + printf " Possible causes for this include:\\n" + printf " - Firewall blocking certain DNS lookups from Pi-hole device\\n" + printf " - ns1.pi-hole.net being blocked (required to obtain TXT record from versions.pi-hole.net containing supported operating systems)\\n" + printf " - Other internet connectivity issues\\n" + else + printf " %b %bUnsupported OS detected: %s %s%b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${detected_os^}" "${detected_version}" "${COL_NC}" + printf " If you are seeing this message and you do have a supported OS, please contact support.\\n" + fi + printf "\\n" + printf " %bhttps://docs.pi-hole.net/main/prerequisites/#supported-operating-systems%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf "\\n" + printf " If you wish to attempt to continue anyway, you can try one of the following commands to skip this check:\\n" + printf "\\n" + printf " e.g: If you are seeing this message on a fresh install, you can run:\\n" + printf " %bcurl -sSL https://install.pi-hole.net | sudo PIHOLE_SKIP_OS_CHECK=true bash%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf "\\n" + printf " If you are seeing this message after having run pihole -up:\\n" + printf " %bsudo PIHOLE_SKIP_OS_CHECK=true pihole -r%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf " (In this case, your previous run of pihole -up will have already updated the local repository)\\n" + printf "\\n" + printf " It is possible that the installation will still fail at this stage due to an unsupported configuration.\\n" + printf " If that is the case, you can feel free to ask the community on Discourse with the %bCommunity Help%b category:\\n" "${COL_LIGHT_RED}" "${COL_NC}" + printf " %bhttps://discourse.pi-hole.net/c/bugs-problems-issues/community-help/%b\\n" "${COL_LIGHT_GREEN}" "${COL_NC}" + printf "\\n" + exit 1 + + else + printf " %b %bSupported OS detected%b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${COL_NC}" + fi + else + printf " %b %bPIHOLE_SKIP_OS_CHECK env variable set to true - installer will continue%b\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}" + fi +} + +# This function waits for dpkg to unlock, which signals that the previous apt-get command has finished. +test_dpkg_lock() { + i=0 + printf " %b Waiting for package manager to finish (up to 30 seconds)\\n" "${INFO}" + # fuser is a program to show which processes use the named files, sockets, or filesystems + # So while the lock is held, + while fuser /var/lib/dpkg/lock >/dev/null 2>&1 + do + # we wait half a second, + sleep 0.5 + # increase the iterator, + ((i=i+1)) + # exit if waiting for more then 30 seconds + if [[ $i -gt 60 ]]; then + printf " %b %bError: Could not verify package manager finished and released lock. %b\\n" "${CROSS}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " Attempt to install packages manually and retry.\\n" + exit 1; + fi + done + # and then report success once dpkg is unlocked. + return 0 +} + +# Compatibility +package_manager_detect() { + # TODO - pull common packages for both distributions out into a common variable, then add + # the distro-specific ones below. + + # First check to see if apt-get is installed. + if is_command apt-get ; then + # Set some global variables here + # We don't set them earlier since the installed package manager might be rpm, so these values would be different + PKG_MANAGER="apt-get" + # A variable to store the command used to update the package cache + UPDATE_PKG_CACHE="${PKG_MANAGER} update" + # The command we will use to actually install packages + PKG_INSTALL=("${PKG_MANAGER}" -qq --no-install-recommends install) + # grep -c will return 1 if there are no matches. This is an acceptable condition, so we OR TRUE to prevent set -e exiting the script. + PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" + # Update package cache + update_package_cache || exit 1 + # Check for and determine version number (major and minor) of current php install + local phpVer="php" + if is_command php ; then + phpVer="$(php <<< "")" + # Check if the first character of the string is numeric + if [[ ${phpVer:0:1} =~ [1-9] ]]; then + printf " %b Existing PHP installation detected : PHP version %s\\n" "${INFO}" "${phpVer}" + printf -v phpInsMajor "%d" "$(php <<< "")" + printf -v phpInsMinor "%d" "$(php <<< "")" + phpVer="php$phpInsMajor.$phpInsMinor" + else + printf " %b No valid PHP installation detected!\\n" "${CROSS}" + printf " %b PHP version : %s\\n" "${INFO}" "${phpVer}" + printf " %b Aborting installation.\\n" "${CROSS}" + exit 1 + fi + fi + # Packages required to perform the os_check (stored as an array) + OS_CHECK_DEPS=(grep dnsutils) + # Packages required to run this install script (stored as an array) + INSTALLER_DEPS=(git iproute2 dialog ca-certificates) + # Packages required to run Pi-hole (stored as an array) + PIHOLE_DEPS=(cron curl iputils-ping psmisc sudo unzip idn2 libcap2-bin dns-root-data libcap2 netcat-openbsd procps jq) + # Packages required for the Web admin interface (stored as an array) + # It's useful to separate this from Pi-hole, since the two repos are also setup separately + PIHOLE_WEB_DEPS=(lighttpd "${phpVer}-common" "${phpVer}-cgi" "${phpVer}-sqlite3" "${phpVer}-xml" "${phpVer}-intl") + # Prior to PHP8.0, JSON functionality is provided as dedicated module, required by Pi-hole AdminLTE: https://www.php.net/manual/json.installation.php + if [[ -z "${phpInsMajor}" || "${phpInsMajor}" -lt 8 ]]; then + PIHOLE_WEB_DEPS+=("${phpVer}-json") + fi + # The Web server user, + LIGHTTPD_USER="www-data" + # group, + LIGHTTPD_GROUP="www-data" + # and config file + LIGHTTPD_CFG="lighttpd.conf.debian" + + # If apt-get is not found, check for rpm. + elif is_command rpm ; then + # Then check if dnf or yum is the package manager + if is_command dnf ; then + PKG_MANAGER="dnf" + else + PKG_MANAGER="yum" + fi + + # These variable names match the ones for apt-get. See above for an explanation of what they are for. + PKG_INSTALL=("${PKG_MANAGER}" install -y) + # CentOS package manager returns 100 when there are packages to update so we need to || true to prevent the script from exiting. + PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l || true" + OS_CHECK_DEPS=(grep bind-utils) + INSTALLER_DEPS=(git dialog iproute newt procps-ng which chkconfig ca-certificates) + PIHOLE_DEPS=(cronie curl findutils sudo unzip libidn2 psmisc libcap nmap-ncat jq) + PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php-common php-cli php-pdo php-xml php-json php-intl) + LIGHTTPD_USER="lighttpd" + LIGHTTPD_GROUP="lighttpd" + LIGHTTPD_CFG="lighttpd.conf.fedora" + + # If the host OS is centos (or a derivative), epel is required for lighttpd + if ! grep -qiE 'fedora|fedberry' /etc/redhat-release; then + if rpm -qa | grep -qi 'epel'; then + printf " %b EPEL repository already installed\\n" "${TICK}" + else + local RH_RELEASE EPEL_PKG + # EPEL not already installed, add it based on the release version + RH_RELEASE=$(grep -oP '(?<= )[0-9]+(?=\.?)' /etc/redhat-release) + EPEL_PKG="https://dl.fedoraproject.org/pub/epel/epel-release-latest-${RH_RELEASE}.noarch.rpm" + printf " %b Enabling EPEL package repository (https://fedoraproject.org/wiki/EPEL)\\n" "${INFO}" + "${PKG_INSTALL[@]}" "${EPEL_PKG}" + printf " %b Installed %s\\n" "${TICK}" "${EPEL_PKG}" + fi + fi + + # If neither apt-get or yum/dnf package managers were found + else + # we cannot install required packages + printf " %b No supported package manager found\\n" "${CROSS}" + # so exit the installer + exit + fi +} + +# A function for checking if a directory is a git repository +is_repo() { + # Use a named, local variable instead of the vague $1, which is the first argument passed to this function + # These local variables should always be lowercase + local directory="${1}" + # A variable to store the return code + local rc + # If the first argument passed to this function is a directory, + if [[ -d "${directory}" ]]; then + # move into the directory + pushd "${directory}" &> /dev/null || return 1 + # Use git to check if the directory is a repo + # git -C is not used here to support git versions older than 1.8.4 + git status --short &> /dev/null || rc=$? + # If the command was not successful, + else + # Set a non-zero return code if directory does not exist + rc=1 + fi + # Move back into the directory the user started in + popd &> /dev/null || return 1 + # Return the code; if one is not set, return 0 + return "${rc:-0}" +} + +# A function to clone a repo +make_repo() { + # Set named variables for better readability + local directory="${1}" + local remoteRepo="${2}" + + # The message to display when this function is running + str="Clone ${remoteRepo} into ${directory}" + # Display the message and use the color table to preface the message with an "info" indicator + printf " %b %s..." "${INFO}" "${str}" + # If the directory exists, + if [[ -d "${directory}" ]]; then + # Return with a 1 to exit the installer. We don't want to overwrite what could already be here in case it is not ours + str="Unable to clone ${remoteRepo} into ${directory} : Directory already exists" + printf "%b %b%s\\n" "${OVER}" "${CROSS}" "${str}" + return 1 + fi + # Clone the repo and return the return code from this command + git clone -q --depth 20 "${remoteRepo}" "${directory}" &> /dev/null || return $? + # Move into the directory that was passed as an argument + pushd "${directory}" &> /dev/null || return 1 + # Check current branch. If it is master, then reset to the latest available tag. + # In case extra commits have been added after tagging/release (i.e in case of metadata updates/README.MD tweaks) + curBranch=$(git rev-parse --abbrev-ref HEAD) + if [[ "${curBranch}" == "master" ]]; then + # If we're calling make_repo() then it should always be master, we may not need to check. + git reset --hard "$(git describe --abbrev=0 --tags)" || return $? + fi + # Show a colored message showing it's status + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) + chmod -R a+rX "${directory}" + # Move back into the original directory + popd &> /dev/null || return 1 + return 0 +} + +# We need to make sure the repos are up-to-date so we can effectively install Clean out the directory if it exists for git to clone into +update_repo() { + # Use named, local variables + # As you can see, these are the same variable names used in the last function, + # but since they are local, their scope does not go beyond this function + # This helps prevent the wrong value from being assigned if you were to set the variable as a GLOBAL one + local directory="${1}" + local curBranch + + # A variable to store the message we want to display; + # Again, it's useful to store these in variables in case we need to reuse or change the message; + # we only need to make one change here + local str="Update repo in ${1}" + # Move into the directory that was passed as an argument + pushd "${directory}" &> /dev/null || return 1 + # Let the user know what's happening + printf " %b %s..." "${INFO}" "${str}" + # Stash any local commits as they conflict with our working code + git stash --all --quiet &> /dev/null || true # Okay for stash failure + git clean --quiet --force -d || true # Okay for already clean directory + # Pull the latest commits + git pull --no-rebase --quiet &> /dev/null || return $? + # Check current branch. If it is master, then reset to the latest available tag. + # In case extra commits have been added after tagging/release (i.e in case of metadata updates/README.MD tweaks) + curBranch=$(git rev-parse --abbrev-ref HEAD) + if [[ "${curBranch}" == "master" ]]; then + git reset --hard "$(git describe --abbrev=0 --tags)" || return $? + fi + # Show a completion message + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) + chmod -R a+rX "${directory}" + # Move back into the original directory + popd &> /dev/null || return 1 + return 0 +} + +# A function that combines the previous git functions to update or clone a repo +getGitFiles() { + # Setup named variables for the git repos + # We need the directory + local directory="${1}" + # as well as the repo URL + local remoteRepo="${2}" + # A local variable containing the message to be displayed + local str="Check for existing repository in ${1}" + # Show the message + printf " %b %s..." "${INFO}" "${str}" + # Check if the directory is a repository + if is_repo "${directory}"; then + # Show that we're checking it + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + # Update the repo, returning an error message on failure + update_repo "${directory}" || { printf "\\n %b: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } + # If it's not a .git repo, + else + # Show an error + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + # Attempt to make the repository, showing an error on failure + make_repo "${directory}" "${remoteRepo}" || { printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } + fi + echo "" + # Success via one of the two branches, as the commands would exit if they failed. + return 0 +} + +# Reset a repo to get rid of any local changed +resetRepo() { + # Use named variables for arguments + local directory="${1}" + # Move into the directory + pushd "${directory}" &> /dev/null || return 1 + # Store the message in a variable + str="Resetting repository within ${1}..." + # Show the message + printf " %b %s..." "${INFO}" "${str}" + # Use git to remove the local changes + git reset --hard &> /dev/null || return $? + # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) + chmod -R a+rX "${directory}" + # And show the status + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + # Return to where we came from + popd &> /dev/null || return 1 + # Function succeeded, as "git reset" would have triggered a return earlier if it failed + return 0 +} + +find_IPv4_information() { + # Detects IPv4 address used for communication to WAN addresses. + # Accepts no arguments, returns no values. + + # Named, local variables + local route + local IPv4bare + + # Find IP used to route to outside world by checking the the route to Google's public DNS server + route=$(ip route get 8.8.8.8) + + # Get just the interface IPv4 address + # shellcheck disable=SC2059,SC2086 + # disabled as we intentionally want to split on whitespace and have printf populate + # the variable with just the first field. + printf -v IPv4bare "$(printf ${route#*src })" + # Get the default gateway IPv4 address (the way to reach the Internet) + # shellcheck disable=SC2059,SC2086 + printf -v IPv4gw "$(printf ${route#*via })" + + if ! valid_ip "${IPv4bare}" ; then + IPv4bare="127.0.0.1" + fi + + # Append the CIDR notation to the IP address, if valid_ip fails this should return 127.0.0.1/8 + IPV4_ADDRESS=$(ip -oneline -family inet address show | grep "${IPv4bare}/" | awk '{print $4}' | awk 'END {print}') +} + +# Get available interfaces that are UP +get_available_interfaces() { + # There may be more than one so it's all stored in a variable + availableInterfaces=$(ip --oneline link show up | grep -v "lo" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1) +} + +# A function for displaying the dialogs the user sees when first running the installer +welcomeDialogs() { + # Display the welcome dialog using an appropriately sized window via the calculation conducted earlier in the script + dialog --no-shadow --clear --keep-tite \ + --backtitle "Welcome" \ + --title "Pi-hole Automated Installer" \ + --msgbox "\\n\\nThis installer will transform your device into a network-wide ad blocker!" \ + "${r}" "${c}" \ + --and-widget --clear \ + --backtitle "Support Pi-hole" \ + --title "Open Source Software" \ + --msgbox "\\n\\nThe Pi-hole is free, but powered by your donations: https://pi-hole.net/donate/" \ + "${r}" "${c}" \ + --and-widget --clear \ + --colors \ + --backtitle "Initiating network interface" \ + --title "Static IP Needed" \ + --no-button "Exit" --yes-button "Continue" \ + --defaultno \ + --yesno "\\n\\nThe Pi-hole is a SERVER so it needs a STATIC IP ADDRESS to function properly.\\n\\n\ +\\Zb\\Z1IMPORTANT:\\Zn If you have not already done so, you must ensure that this device has a static IP.\\n\\n\ +Depending on your operating system, there are many ways to achieve this, through DHCP reservation, or by manually assigning one.\\n\\n\ +Please continue when the static addressing has been configured."\ + "${r}" "${c}" && result=0 || result="$?" + + case "${result}" in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Installer exited at static IP message.\\n" "${INFO}" + exit 1 + ;; + esac +} + +# A function that lets the user pick an interface to use with Pi-hole +chooseInterface() { + # Turn the available interfaces into a string so it can be used with dialog + local interfacesList + # Number of available interfaces + local interfaceCount + + # POSIX compliant way to get the number of elements in an array + interfaceCount=$(printf "%s\n" "${availableInterfaces}" | wc -l) + + # If there is one interface, + if [[ "${interfaceCount}" -eq 1 ]]; then + # Set it as the interface to use since there is no other option + PIHOLE_INTERFACE="${availableInterfaces}" + # Otherwise, + else + # Set status for the first entry to be selected + status="ON" + + # While reading through the available interfaces + for interface in ${availableInterfaces}; do + # Put all these interfaces into a string + interfacesList="${interfacesList}${interface} available ${status} " + # All further interfaces are deselected + status="OFF" + done + # shellcheck disable=SC2086 + # Disable check for double quote here as we are passing a string with spaces + PIHOLE_INTERFACE=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" --ok-label "Select" \ + --radiolist "Choose An Interface (press space to toggle selection)" \ + ${r} ${c} "${interfaceCount}" ${interfacesList}) + + result=$? + case ${result} in + "${DIALOG_CANCEL}"|"${DIALOG_ESC}") + # Show an error message and exit + printf " %b %s\\n" "${CROSS}" "No interface selected, exiting installer" + exit 1 + ;; + esac + + printf " %b Using interface: %s\\n" "${INFO}" "${PIHOLE_INTERFACE}" + fi +} + +# This lets us prefer ULA addresses over GUA +# This caused problems for some users when their ISP changed their IPv6 addresses +# See https://github.com/pi-hole/pi-hole/issues/1473#issuecomment-301745953 +testIPv6() { + # first will contain fda2 (ULA) + printf -v first "%s" "${1%%:*}" + # value1 will contain 253 which is the decimal value corresponding to 0xFD + value1=$(( (0x$first)/256 )) + # value2 will contain 162 which is the decimal value corresponding to 0xA2 + value2=$(( (0x$first)%256 )) + # the ULA test is testing for fc00::/7 according to RFC 4193 + if (( (value1&254)==252 )); then + # echoing result to calling function as return value + echo "ULA" + fi + # the GUA test is testing for 2000::/3 according to RFC 4291 + if (( (value1&112)==32 )); then + # echoing result to calling function as return value + echo "GUA" + fi + # the LL test is testing for fe80::/10 according to RFC 4193 + if (( (value1)==254 )) && (( (value2&192)==128 )); then + # echoing result to calling function as return value + echo "Link-local" + fi +} + +find_IPv6_information() { + # Detects IPv6 address used for communication to WAN addresses. + mapfile -t IPV6_ADDRESSES <<<"$(ip -6 address | grep 'scope global' | awk '{print $2}')" + + # For each address in the array above, determine the type of IPv6 address it is + for i in "${IPV6_ADDRESSES[@]}"; do + # Check if it's ULA, GUA, or LL by using the function created earlier + result=$(testIPv6 "$i") + # If it's a ULA address, use it and store it as a global variable + [[ "${result}" == "ULA" ]] && ULA_ADDRESS="${i%/*}" + # If it's a GUA address, use it and store it as a global variable + [[ "${result}" == "GUA" ]] && GUA_ADDRESS="${i%/*}" + # Else if it's a Link-local address, we cannot use it, so just continue + done + + # Determine which address to be used: Prefer ULA over GUA or don't use any if none found + # If the ULA_ADDRESS contains a value, + if [[ -n "${ULA_ADDRESS}" ]]; then + # set the IPv6 address to the ULA address + IPV6_ADDRESS="${ULA_ADDRESS}" + # Show this info to the user + printf " %b Found IPv6 ULA address\\n" "${INFO}" + # Otherwise, if the GUA_ADDRESS has a value, + elif [[ -n "${GUA_ADDRESS}" ]]; then + # Let the user know + printf " %b Found IPv6 GUA address\\n" "${INFO}" + # And assign it to the global variable + IPV6_ADDRESS="${GUA_ADDRESS}" + # If none of those work, + else + printf " %b Unable to find IPv6 ULA/GUA address\\n" "${INFO}" + # So set the variable to be empty + IPV6_ADDRESS="" + fi +} + +# A function to collect IPv4 and IPv6 information of the device +collect_v4andv6_information() { + find_IPv4_information + # Echo the information to the user + printf " %b IPv4 address: %s\\n" "${INFO}" "${IPV4_ADDRESS}" + # if `dhcpcd` is used offer to set this as static IP for the device + if [[ -f "/etc/dhcpcd.conf" ]]; then + # configure networking via dhcpcd + getStaticIPv4Settings + fi + find_IPv6_information + printf " %b IPv6 address: %s\\n" "${INFO}" "${IPV6_ADDRESS}" +} + +getStaticIPv4Settings() { + # Local, named variables + local ipSettingsCorrect + local DHCPChoice + # Ask if the user wants to use DHCP settings as their static IP + # This is useful for users that are using DHCP reservations; we can use the information gathered + DHCPChoice=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" --ok-label "Continue" \ + --backtitle "Calibrating network interface" \ + --title "Static IP Address" \ + --menu "Do you want to use your current network settings as a static address?\\n \ + IP address: ${IPV4_ADDRESS}\\n \ + Gateway: ${IPv4gw}\\n" \ + "${r}" "${c}" 3 \ + "Yes" "Set static IP using current values" \ + "No" "Set static IP using custom values" \ + "Skip" "I will set a static IP later, or have already done so") + + result=$? + case ${result} in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + + case ${DHCPChoice} in + "Skip") + return + ;; + "Yes") + # If they choose yes, let the user know that the IP address will not be available via DHCP and may cause a conflict. + dialog --no-shadow --keep-tite \ + --cancel-label "Exit" \ + --backtitle "IP information" \ + --title "FYI: IP Conflict" \ + --msgbox "\\nIt 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.\n\n\ +If you are worried, either manually set the address, or modify the DHCP reservation pool so it does not include the IP you want.\n\n\ +It is also possible to use a DHCP reservation, but if you are going to do that, you might as well set a static address."\ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + ;; + + "No") + # Otherwise, we need to ask the user to input their desired settings. + # 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 + ipSettingsCorrect=false + until [[ "${ipSettingsCorrect}" = True ]]; do + + # Ask for the IPv4 address + _staticIPv4Temp=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --ok-label "Continue" \ + --backtitle "Calibrating network interface" \ + --title "IPv4 Address" \ + --form "\\nEnter your desired IPv4 address" \ + "${r}" "${c}" 0 \ + "IPv4 Address:" 1 1 "${IPV4_ADDRESS}" 1 15 19 0 \ + "IPv4 Gateway:" 2 1 "${IPv4gw}" 2 15 19 0) + + result=$? + case ${result} in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + + IPV4_ADDRESS=${_staticIPv4Temp%$'\n'*} + IPv4gw=${_staticIPv4Temp#*$'\n'} + + # Give the user a chance to review their settings before moving on + dialog --no-shadow --keep-tite \ + --no-label "Edit IP" \ + --backtitle "Calibrating network interface" \ + --title "Static IP Address" \ + --defaultno \ + --yesno "Are these settings correct? + IP address: ${IPV4_ADDRESS} + Gateway: ${IPv4gw}" \ + "${r}" "${c}" && ipSettingsCorrect=True + done + ;; + esac + setDHCPCD +} + +# Configure networking via dhcpcd +setDHCPCD() { + # Check if the IP is already in the file + if grep -q "${IPV4_ADDRESS}" /etc/dhcpcd.conf; then + printf " %b Static IP already configured\\n" "${INFO}" + # If it's not, + else + # we can append these lines to dhcpcd.conf to enable a static IP + echo "interface ${PIHOLE_INTERFACE} + static ip_address=${IPV4_ADDRESS} + static routers=${IPv4gw} + static domain_name_servers=${PIHOLE_DNS_1} ${PIHOLE_DNS_2}" | tee -a /etc/dhcpcd.conf >/dev/null + # Then use the ip command to immediately set the new address + ip addr replace dev "${PIHOLE_INTERFACE}" "${IPV4_ADDRESS}" + # Also give a warning that the user may need to reboot their system + printf " %b Set IP address to %s\\n" "${TICK}" "${IPV4_ADDRESS%/*}" + printf " %b You may need to restart after the install is complete\\n" "${INFO}" + fi +} + +# Check an IP address to see if it is a valid one +valid_ip() { + # Local, named variables + local ip=${1} + local stat=1 + + # Regex matching one IPv4 component, i.e. an integer from 0 to 255. + # See https://tools.ietf.org/html/rfc1340 + local ipv4elem="(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)"; + # Regex matching an optional port (starting with '#') range of 1-65536 + local portelem="(#(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}|0))?"; + # Build a full IPv4 regex from the above subexpressions + local regex="^${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}\\.${ipv4elem}${portelem}$" + + # Evaluate the regex, and return the result + [[ $ip =~ ${regex} ]] + + stat=$? + return "${stat}" +} + +valid_ip6() { + local ip=${1} + local stat=1 + + # Regex matching one IPv6 element, i.e. a hex value from 0000 to FFFF + local ipv6elem="[0-9a-fA-F]{1,4}" + # Regex matching an IPv6 CIDR, i.e. 1 to 128 + local v6cidr="(\\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}" + # Regex matching an optional port (starting with '#') range of 1-65536 + local portelem="(#(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}|0))?"; + # Build a full IPv6 regex from the above subexpressions + local regex="^(((${ipv6elem}))*((:${ipv6elem}))*::((${ipv6elem}))*((:${ipv6elem}))*|((${ipv6elem}))((:${ipv6elem})){7})${v6cidr}${portelem}$" + + # Evaluate the regex, and return the result + [[ ${ip} =~ ${regex} ]] + + stat=$? + return "${stat}" +} + +# A function to choose the upstream DNS provider(s) +setDNS() { + # Local, named variables + local DNSSettingsCorrect + + # In an array, list the available upstream providers + DNSChooseOptions=() + local DNSServerCount=0 + # Save the old Internal Field Separator in a variable, + OIFS=$IFS + # and set the new one to newline + IFS=$'\n' + # Put the DNS Servers into an array + for DNSServer in ${DNS_SERVERS} + do + DNSName="$(cut -d';' -f1 <<< "${DNSServer}")" + DNSChooseOptions[DNSServerCount]="${DNSName}" + (( DNSServerCount=DNSServerCount+1 )) + DNSChooseOptions[DNSServerCount]="" + (( DNSServerCount=DNSServerCount+1 )) + done + DNSChooseOptions[DNSServerCount]="Custom" + (( DNSServerCount=DNSServerCount+1 )) + DNSChooseOptions[DNSServerCount]="" + # Restore the IFS to what it was + IFS=${OIFS} + # In a dialog, show the options + DNSchoices=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --menu "Select Upstream DNS Provider. To use your own, select Custom." "${r}" "${c}" 7 \ + "${DNSChooseOptions[@]}") + + result=$? + case ${result} in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + + # Depending on the user's choice, set the GLOBAL variables to the IP of the respective provider + if [[ "${DNSchoices}" == "Custom" ]] + then + # Loop until we have a valid DNS setting + until [[ "${DNSSettingsCorrect}" = True ]]; do + # Signal value, to be used if the user inputs an invalid IP address + strInvalid="Invalid" + if [[ ! "${PIHOLE_DNS_1}" ]]; then + if [[ ! "${PIHOLE_DNS_2}" ]]; then + # If the first and second upstream servers do not exist, do not prepopulate an IP address + prePopulate="" + else + # Otherwise, prepopulate the dialogue with the appropriate DNS value(s) + 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 + + # Prompt the user to enter custom upstream servers + piholeDNS=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --backtitle "Specify Upstream DNS Provider(s)" \ + --inputbox "Enter your desired upstream DNS provider(s), separated by a comma.\ +If you want to specify a port other than 53, separate it with a hash.\ +\\n\\nFor example '8.8.8.8, 8.8.4.4' or '127.0.0.1#5335'"\ + "${r}" "${c}" "${prePopulate}") + + result=$? + case ${result} in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + + # Clean user input and replace whitespace with comma. + piholeDNS=$(sed 's/[, \t]\+/,/g' <<< "${piholeDNS}") + + # Separate the user input into the two DNS values (separated by a comma) + printf -v PIHOLE_DNS_1 "%s" "${piholeDNS%%,*}" + printf -v PIHOLE_DNS_2 "%s" "${piholeDNS##*,}" + + # If the first DNS value is invalid or empty, this if statement will be true and we will set PIHOLE_DNS_1="Invalid" + if ! valid_ip "${PIHOLE_DNS_1}" || [[ ! "${PIHOLE_DNS_1}" ]]; then + PIHOLE_DNS_1=${strInvalid} + fi + # If the second DNS value is invalid or empty, this if statement will be true and we will set PIHOLE_DNS_2="Invalid" + if ! valid_ip "${PIHOLE_DNS_2}" && [[ "${PIHOLE_DNS_2}" ]]; then + PIHOLE_DNS_2=${strInvalid} + fi + # If either of the DNS servers are invalid, + if [[ "${PIHOLE_DNS_1}" == "${strInvalid}" ]] || [[ "${PIHOLE_DNS_2}" == "${strInvalid}" ]]; then + # explain this to the user, + dialog --no-shadow --keep-tite \ + --title "Invalid IP Address(es)" \ + --backtitle "Invalid IP" \ + --msgbox "\\nOne or both of the entered IP addresses were invalid. Please try again.\ +\\n\\nInvalid IPs: ${PIHOLE_DNS_1}, ${PIHOLE_DNS_2}" \ + "${r}" "${c}" + + # set the variables back to nothing, + if [[ "${PIHOLE_DNS_1}" == "${strInvalid}" ]]; then + PIHOLE_DNS_1="" + fi + if [[ "${PIHOLE_DNS_2}" == "${strInvalid}" ]]; then + PIHOLE_DNS_2="" + fi + # and continue the loop. + DNSSettingsCorrect=False + else + dialog --no-shadow --keep-tite \ + --backtitle "Specify Upstream DNS Provider(s)" \ + --title "Upstream DNS Provider(s)" \ + --yesno "Are these settings correct?\\n\\tDNS Server 1:\\t${PIHOLE_DNS_1}\\n\\tDNS Server 2:\\t${PIHOLE_DNS_2}" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + DNSSettingsCorrect=True + ;; + "${DIALOG_CANCEL}") + DNSSettingsCorrect=False + ;; + "${DIALOG_ESC}") + printf " %b Escape pressed, exiting installer at DNS Settings%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + fi + done + else + # Save the old Internal Field Separator in a variable, + OIFS=$IFS + # and set the new one to newline + IFS=$'\n' + for DNSServer in ${DNS_SERVERS} + do + DNSName="$(cut -d';' -f1 <<< "${DNSServer}")" + if [[ "${DNSchoices}" == "${DNSName}" ]] + then + PIHOLE_DNS_1="$(cut -d';' -f2 <<< "${DNSServer}")" + PIHOLE_DNS_2="$(cut -d';' -f3 <<< "${DNSServer}")" + break + fi + done + # Restore the IFS to what it was + IFS=${OIFS} + fi + + # Display final selection + local DNSIP=${PIHOLE_DNS_1} + [[ -z ${PIHOLE_DNS_2} ]] || DNSIP+=", ${PIHOLE_DNS_2}" + printf " %b Using upstream DNS: %s (%s)\\n" "${INFO}" "${DNSchoices}" "${DNSIP}" +} + +# Allow the user to enable/disable logging +setLogging() { + # Ask the user if they want to enable logging + dialog --no-shadow --keep-tite \ + --backtitle "Pihole Installation" \ + --title "Enable Logging" \ + --yesno "\\n\\nWould you like to enable query logging?" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Query Logging on.\\n" "${INFO}" + QUERY_LOGGING=true + ;; + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Query Logging off.\\n" "${INFO}" + QUERY_LOGGING=false + ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at Query Logging choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac +} + +# Allow the user to set their FTL privacy level +setPrivacyLevel() { + # The default selection is level 0 + PRIVACY_LEVEL=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label "Exit" \ + --ok-label "Continue" \ + --radiolist "Select a privacy mode for FTL. https://docs.pi-hole.net/ftldns/privacylevels/" \ + "${r}" "${c}" 6 \ + "0" "Show everything" on \ + "1" "Hide domains" off \ + "2" "Hide domains and clients" off \ + "3" "Anonymous mode" off) + + result=$? + case ${result} in + "${DIALOG_OK}") + printf " %b Using privacy level: %s\\n" "${INFO}" "${PRIVACY_LEVEL}" + ;; + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancelled privacy level selection.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac +} + +# Function to ask the user if they want to install the dashboard +setAdminFlag() { + # Similar to the logging function, ask what the user wants + dialog --no-shadow --keep-tite \ + --backtitle "Pihole Installation" \ + --title "Admin Web Interface" \ + --yesno "\\n\\nDo you want to install the Admin Web Interface?" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Installing Admin Web Interface\\n" "${INFO}" + # Set the flag to install the web interface + INSTALL_WEB_INTERFACE=true + ;; + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Not installing Admin Web Interface\\n" "${INFO}" + # Set the flag to not install the web interface + INSTALL_WEB_INTERFACE=false + INSTALL_WEB_SERVER=false + ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at Admin Web Interface choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + + # If the user wants to install the Web admin interface (i.e. it has not been deselected above) and did not deselect the web server via command-line argument + if [[ "${INSTALL_WEB_INTERFACE}" == true && "${INSTALL_WEB_SERVER}" == true ]]; then + # Get list of required PHP modules, excluding base package (common) and handler (cgi) + local i php_modules + for i in "${PIHOLE_WEB_DEPS[@]}"; do [[ $i == 'php'* && $i != *'-common' && $i != *'-cgi' ]] && php_modules+=" ${i#*-}"; done + dialog --no-shadow --keep-tite \ + --backtitle "Pi-hole Installation" \ + --title "Web Server" \ + --yesno "\\n\\nA web server is required for the Admin Web Interface.\ +\\n\\nDo you want to install lighttpd and the required PHP modules?\ +\\n\\nNB: If you disable this, and, do not have an existing web server \ +and required PHP modules (${php_modules# }) installed, the web interface \ +will not function. Additionally the web server user needs to be member of \ +the \"pihole\" group for full functionality." \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Installing lighttpd\\n" "${INFO}" + # Set the flag to install the web server + INSTALL_WEB_SERVER=true + ;; + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Not installing lighttpd\\n" "${INFO}" + # Set the flag to not install the web server + INSTALL_WEB_SERVER=false + ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at web server choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + fi +} + +# A function to display a list of example blocklists for users to select +chooseBlocklists() { + # Back up any existing adlist file, on the off chance that it exists. Useful in case of a reconfigure. + if [[ -f "${adlistFile}" ]]; then + mv "${adlistFile}" "${adlistFile}.old" + fi + # Let user select (or not) blocklists + dialog --no-shadow --keep-tite \ + --backtitle "Pi-hole Installation" \ + --title "Blocklists" \ + --yesno "\\nPi-hole relies on third party lists in order to block ads.\ +\\n\\nYou can use the suggestion below, and/or add your own after installation.\ +\\n\\nSelect 'Yes' to include:\ +\\n\\nStevenBlack's Unified Hosts List" \ + "${r}" "${c}" && result=0 || result=$? + + case ${result} in + "${DIALOG_OK}") + # If they chose yes, + printf " %b Installing StevenBlack's Unified Hosts List\\n" "${INFO}" + echo "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" >> "${adlistFile}" + ;; + "${DIALOG_CANCEL}") + # If they chose no, + printf " %b Not installing StevenBlack's Unified Hosts List\\n" "${INFO}" + ;; + "${DIALOG_ESC}") + # User pressed + printf " %b Escape pressed, exiting installer at blocklist choice.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + # Create an empty adList file with appropriate permissions. + if [ ! -f "${adlistFile}" ]; then + install -m 644 /dev/null "${adlistFile}" + else + chmod 644 "${adlistFile}" + fi +} + +# Used only in unattended setup +# If there is already the adListFile, we keep it, else we create it using all default lists +installDefaultBlocklists() { + # In unattended setup, could be useful to use userdefined blocklist. + # If this file exists, we avoid overriding it. + if [[ -f "${adlistFile}" ]]; then + return; + fi + echo "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" >> "${adlistFile}" +} + +# Check if /etc/dnsmasq.conf is from pi-hole. If so replace with an original and install new in .d directory +version_check_dnsmasq() { + # Local, named variables + local dnsmasq_conf="/etc/dnsmasq.conf" + local dnsmasq_conf_orig="/etc/dnsmasq.conf.orig" + local dnsmasq_pihole_id_string="addn-hosts=/etc/pihole/gravity.list" + local dnsmasq_pihole_id_string2="# Dnsmasq config for Pi-hole's FTLDNS" + local dnsmasq_original_config="${PI_HOLE_LOCAL_REPO}/advanced/dnsmasq.conf.original" + local dnsmasq_pihole_01_source="${PI_HOLE_LOCAL_REPO}/advanced/01-pihole.conf" + local dnsmasq_pihole_01_target="/etc/dnsmasq.d/01-pihole.conf" + local dnsmasq_rfc6761_06_source="${PI_HOLE_LOCAL_REPO}/advanced/06-rfc6761.conf" + local dnsmasq_rfc6761_06_target="/etc/dnsmasq.d/06-rfc6761.conf" + + # If the dnsmasq config file exists + if [[ -f "${dnsmasq_conf}" ]]; then + printf " %b Existing dnsmasq.conf found..." "${INFO}" + # If a specific string is found within this file, we presume it's from older versions on Pi-hole, + if grep -q "${dnsmasq_pihole_id_string}" "${dnsmasq_conf}" || + grep -q "${dnsmasq_pihole_id_string2}" "${dnsmasq_conf}"; then + printf " it is from a previous Pi-hole install.\\n" + printf " %b Backing up dnsmasq.conf to dnsmasq.conf.orig..." "${INFO}" + # so backup the original file, + mv -f "${dnsmasq_conf}" "${dnsmasq_conf_orig}" + printf "%b %b Backing up dnsmasq.conf to dnsmasq.conf.orig...\\n" "${OVER}" "${TICK}" + printf " %b Restoring default dnsmasq.conf..." "${INFO}" + # and replace it with the default + install -D -m 644 -T "${dnsmasq_original_config}" "${dnsmasq_conf}" + printf "%b %b Restoring default dnsmasq.conf...\\n" "${OVER}" "${TICK}" + else + # Otherwise, don't to anything + printf " it is not a Pi-hole file, leaving alone!\\n" + fi + else + # If a file cannot be found, + printf " %b No dnsmasq.conf found... restoring default dnsmasq.conf..." "${INFO}" + # restore the default one + install -D -m 644 -T "${dnsmasq_original_config}" "${dnsmasq_conf}" + printf "%b %b No dnsmasq.conf found... restoring default dnsmasq.conf...\\n" "${OVER}" "${TICK}" + fi + + printf " %b Installing %s..." "${INFO}" "${dnsmasq_pihole_01_target}" + # Check to see if dnsmasq directory exists (it may not due to being a fresh install and dnsmasq no longer being a dependency) + if [[ ! -d "/etc/dnsmasq.d" ]];then + install -d -m 755 "/etc/dnsmasq.d" + fi + # Copy the new Pi-hole DNS config file into the dnsmasq.d directory + install -D -m 644 -T "${dnsmasq_pihole_01_source}" "${dnsmasq_pihole_01_target}" + printf "%b %b Installed %s\n" "${OVER}" "${TICK}" "${dnsmasq_pihole_01_target}" + # Replace our placeholder values with the GLOBAL DNS variables that we populated earlier + # First, swap in the interface to listen on, + sed -i "s/@INT@/$PIHOLE_INTERFACE/" "${dnsmasq_pihole_01_target}" + if [[ "${PIHOLE_DNS_1}" != "" ]]; then + # then swap in the primary DNS server. + sed -i "s/@DNS1@/$PIHOLE_DNS_1/" "${dnsmasq_pihole_01_target}" + else + # Otherwise, remove the line which sets DNS1. + sed -i '/^server=@DNS1@/d' "${dnsmasq_pihole_01_target}" + fi + # Ditto if DNS2 is not empty + if [[ "${PIHOLE_DNS_2}" != "" ]]; then + sed -i "s/@DNS2@/$PIHOLE_DNS_2/" "${dnsmasq_pihole_01_target}" + else + sed -i '/^server=@DNS2@/d' "${dnsmasq_pihole_01_target}" + fi + + # Set the cache size + sed -i "s/@CACHE_SIZE@/$CACHE_SIZE/" "${dnsmasq_pihole_01_target}" + + sed -i 's/^#conf-dir=\/etc\/dnsmasq.d$/conf-dir=\/etc\/dnsmasq.d/' "${dnsmasq_conf}" + + # If the user does not want to enable logging, + if [[ "${QUERY_LOGGING}" == false ]] ; then + # disable it by commenting out the directive in the DNS config file + sed -i 's/^log-queries/#log-queries/' "${dnsmasq_pihole_01_target}" + else + # Otherwise, enable it by uncommenting the directive in the DNS config file + sed -i 's/^#log-queries/log-queries/' "${dnsmasq_pihole_01_target}" + fi + + printf " %b Installing %s..." "${INFO}" "${dnsmasq_rfc6761_06_source}" + install -D -m 644 -T "${dnsmasq_rfc6761_06_source}" "${dnsmasq_rfc6761_06_target}" + printf "%b %b Installed %s\n" "${OVER}" "${TICK}" "${dnsmasq_rfc6761_06_target}" +} + +# Clean an existing installation to prepare for upgrade/reinstall +clean_existing() { + # Local, named variables + # ${1} Directory to clean + local clean_directory="${1}" + # Pop the first argument, and shift all addresses down by one (i.e. ${2} becomes ${1}) + shift + # Then, we can access all arguments ($@) without including the directory to clean + local old_files=( "$@" ) + + # Remove each script in the old_files array + for script in "${old_files[@]}"; do + rm -f "${clean_directory}/${script}.sh" + done +} + +# Install the scripts from repository to their various locations +installScripts() { + # Local, named variables + local str="Installing scripts from ${PI_HOLE_LOCAL_REPO}" + printf " %b %s..." "${INFO}" "${str}" + + # Clear out script files from Pi-hole scripts directory. + clean_existing "${PI_HOLE_INSTALL_DIR}" "${PI_HOLE_FILES[@]}" + + # Install files from local core repository + if is_repo "${PI_HOLE_LOCAL_REPO}"; then + # move into the directory + cd "${PI_HOLE_LOCAL_REPO}" + # Install the scripts by: + # -o setting the owner to the user + # -Dm755 create all leading components of destination except the last, then copy the source to the destination and setting the permissions to 755 + # + # This first one is the directory + install -o "${USER}" -Dm755 -d "${PI_HOLE_INSTALL_DIR}" + # The rest are the scripts Pi-hole needs + 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 "${PI_HOLE_INSTALL_DIR}" ./advanced/Scripts/COL_TABLE + install -o "${USER}" -Dm755 -t "${PI_HOLE_BIN_DIR}" pihole + install -Dm644 ./advanced/bash-completion/pihole /etc/bash_completion.d/pihole + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + + else + # Otherwise, show an error and exit + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + printf "\\t\\t%bError: Local repo %s not found, exiting installer%b\\n" "${COL_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}" + return 1 + fi +} + +# Install the configs from PI_HOLE_LOCAL_REPO to their various locations +installConfigs() { + printf "\\n %b Installing configs from %s...\\n" "${INFO}" "${PI_HOLE_LOCAL_REPO}" + # Make sure Pi-hole's config files are in place + version_check_dnsmasq + + # Install list of DNS servers + # Format: Name;Primary IPv4;Secondary IPv4;Primary IPv6;Secondary IPv6 + # Some values may be empty (for example: DNS servers without IPv6 support) + echo "${DNS_SERVERS}" > "${PI_HOLE_CONFIG_DIR}/dns-servers.conf" + chmod 644 "${PI_HOLE_CONFIG_DIR}/dns-servers.conf" + + # Install template file if it does not exist + if [[ ! -r "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" ]]; then + install -d -m 0755 ${PI_HOLE_CONFIG_DIR} + if ! install -T -o pihole -m 664 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.conf" "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" &>/dev/null; then + printf " %b Error: Unable to initialize configuration file %s/pihole-FTL.conf\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}" + return 1 + fi + fi + + # Install empty custom.list file if it does not exist + if [[ ! -r "${PI_HOLE_CONFIG_DIR}/custom.list" ]]; then + if ! install -o root -m 644 /dev/null "${PI_HOLE_CONFIG_DIR}/custom.list" &>/dev/null; then + printf " %b Error: Unable to initialize configuration file %s/custom.list\\n" "${COL_LIGHT_RED}" "${PI_HOLE_CONFIG_DIR}" + return 1 + fi + fi + + # Install pihole-FTL.service + install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole-FTL.service" "/etc/init.d/pihole-FTL" + + # If the user chose to install the dashboard, + if [[ "${INSTALL_WEB_SERVER}" == true ]]; then + # and if the Web server conf directory does not exist, + if [[ ! -d "/etc/lighttpd" ]]; then + # make it and set the owners + install -d -m 755 -o "${USER}" -g root /etc/lighttpd + # Otherwise, if the config file already exists + elif [[ -f "${lighttpdConfig}" ]]; then + # back up the original + mv "${lighttpdConfig}"{,.orig} + fi + # and copy in the config file Pi-hole needs + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/${LIGHTTPD_CFG} "${lighttpdConfig}" + # Make sure the external.conf file exists, as lighttpd v1.4.50 crashes without it + if [ ! -f /etc/lighttpd/external.conf ]; then + install -m 644 /dev/null /etc/lighttpd/external.conf + fi + # If there is a custom block page in the html/pihole directory, replace 404 handler in lighttpd config + if [[ -f "${PI_HOLE_404_DIR}/custom.php" ]]; then + sed -i 's/^\(server\.error-handler-404\s*=\s*\).*$/\1"\/pihole\/custom\.php"/' "${lighttpdConfig}" + fi + # Make the directories if they do not exist and set the owners + mkdir -p /run/lighttpd + chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /run/lighttpd + mkdir -p /var/cache/lighttpd/compress + chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/compress + mkdir -p /var/cache/lighttpd/uploads + chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/uploads + fi +} + +install_manpage() { + # Copy Pi-hole man pages and call mandb to update man page database + # Default location for man files for /usr/local/bin is /usr/local/share/man + # on lightweight systems may not be present, so check before copying. + printf " %b Testing man page installation" "${INFO}" + if ! is_command mandb ; then + # if mandb is not present, no manpage support + printf "%b %b man not installed\\n" "${OVER}" "${INFO}" + return + elif [[ ! -d "/usr/local/share/man" ]]; then + # appropriate directory for Pi-hole's man page is not present + printf "%b %b man pages not installed\\n" "${OVER}" "${INFO}" + return + fi + if [[ ! -d "/usr/local/share/man/man8" ]]; then + # if not present, create man8 directory + install -d -m 755 /usr/local/share/man/man8 + fi + if [[ ! -d "/usr/local/share/man/man5" ]]; then + # if not present, create man5 directory + install -d -m 755 /usr/local/share/man/man5 + fi + # Testing complete, copy the files & update the man db + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/manpages/pihole.8 /usr/local/share/man/man8/pihole.8 + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/manpages/pihole-FTL.8 /usr/local/share/man/man8/pihole-FTL.8 + + # remove previously installed "pihole-FTL.conf.5" man page + if [[ -f "/usr/local/share/man/man5/pihole-FTL.conf.5" ]]; then + rm /usr/local/share/man/man5/pihole-FTL.conf.5 + fi + + if mandb -q &>/dev/null; then + # Updated successfully + printf "%b %b man pages installed and database updated\\n" "${OVER}" "${TICK}" + return + else + # Something is wrong with the system's man installation, clean up + # our files, (leave everything how we found it). + rm /usr/local/share/man/man8/pihole.8 /usr/local/share/man/man8/pihole-FTL.8 + printf "%b %b man page db not updated, man pages not installed\\n" "${OVER}" "${CROSS}" + fi +} + +stop_service() { + # Stop service passed in as argument. + # Can softfail, as process may not be installed when this is called + local str="Stopping ${1} service" + printf " %b %s..." "${INFO}" "${str}" + if is_command systemctl ; then + systemctl stop "${1}" &> /dev/null || true + else + service "${1}" stop &> /dev/null || true + fi + printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" +} + +# Start/Restart service passed in as argument +restart_service() { + # Local, named variables + local str="Restarting ${1} service" + printf " %b %s..." "${INFO}" "${str}" + # If systemctl exists, + if is_command systemctl ; then + # use that to restart the service + systemctl restart "${1}" &> /dev/null + else + # Otherwise, fall back to the service command + service "${1}" restart &> /dev/null + fi + printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" +} + +# Enable service so that it will start with next reboot +enable_service() { + # Local, named variables + local str="Enabling ${1} service to start on reboot" + printf " %b %s..." "${INFO}" "${str}" + # If systemctl exists, + if is_command systemctl ; then + # use that to enable the service + systemctl enable "${1}" &> /dev/null + else + # Otherwise, use update-rc.d to accomplish this + update-rc.d "${1}" defaults &> /dev/null + fi + printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" +} + +# Disable service so that it will not with next reboot +disable_service() { + # Local, named variables + local str="Disabling ${1} service" + printf " %b %s..." "${INFO}" "${str}" + # If systemctl exists, + if is_command systemctl ; then + # use that to disable the service + systemctl disable "${1}" &> /dev/null + else + # Otherwise, use update-rc.d to accomplish this + update-rc.d "${1}" disable &> /dev/null + fi + printf "%b %b %s...\\n" "${OVER}" "${TICK}" "${str}" +} + +check_service_active() { + # If systemctl exists, + if is_command systemctl ; then + # use that to check the status of the service + systemctl is-enabled "${1}" &> /dev/null + else + # Otherwise, fall back to service command + service "${1}" status &> /dev/null + fi +} + +# Systemd-resolved's DNSStubListener and dnsmasq can't share port 53. +disable_resolved_stublistener() { + printf " %b Testing if systemd-resolved is enabled\\n" "${INFO}" + # Check if Systemd-resolved's DNSStubListener is enabled and active on port 53 + if check_service_active "systemd-resolved"; then + # Check if DNSStubListener is enabled + printf " %b %b Testing if systemd-resolved DNSStub-Listener is active" "${OVER}" "${INFO}" + if ( grep -E '#?DNSStubListener=yes' /etc/systemd/resolved.conf &> /dev/null ); then + # Disable the DNSStubListener to unbind it from port 53 + # Note that this breaks dns functionality on host until dnsmasq/ftl are up and running + printf "%b %b Disabling systemd-resolved DNSStubListener" "${OVER}" "${TICK}" + # Make a backup of the original /etc/systemd/resolved.conf + # (This will need to be restored on uninstallation) + sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf + printf " and restarting systemd-resolved\\n" + systemctl reload-or-restart systemd-resolved + else + printf "%b %b Systemd-resolved does not need to be restarted\\n" "${OVER}" "${INFO}" + fi + else + printf "%b %b Systemd-resolved is not enabled\\n" "${OVER}" "${INFO}" + fi +} + +update_package_cache() { + # Update package cache on apt based OSes. Do this every time since + # it's quick and packages can be updated at any time. + + # Local, named variables + local str="Update local cache of available packages" + printf " %b %s..." "${INFO}" "${str}" + # Create a command from the package cache variable + if eval "${UPDATE_PKG_CACHE}" &> /dev/null; then + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + # Otherwise, show an error and exit + + # In case we used apt-get and apt is also available, we use this as recommendation as we have seen it + # gives more user-friendly (interactive) advice + if [[ ${PKG_MANAGER} == "apt-get" ]] && is_command apt ; then + UPDATE_PKG_CACHE="apt update" + fi + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + printf " %b Error: Unable to update package cache. Please try \"%s\"%b\\n" "${COL_LIGHT_RED}" "sudo ${UPDATE_PKG_CACHE}" "${COL_NC}" + return 1 + fi +} + +# Let user know if they have outdated packages on their system and +# advise them to run a package update at soonest possible. +notify_package_updates_available() { + # Local, named variables + local str="Checking ${PKG_MANAGER} for upgraded packages" + printf "\\n %b %s..." "${INFO}" "${str}" + # Store the list of packages in a variable + updatesToInstall=$(eval "${PKG_COUNT}") + + if [[ -d "/lib/modules/$(uname -r)" ]]; then + if [[ "${updatesToInstall}" -eq 0 ]]; then + printf "%b %b %s... up to date!\\n\\n" "${OVER}" "${TICK}" "${str}" + else + printf "%b %b %s... %s updates available\\n" "${OVER}" "${TICK}" "${str}" "${updatesToInstall}" + printf " %b %bIt is recommended to update your OS after installing the Pi-hole!%b\\n\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${COL_NC}" + fi + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + printf " Kernel update detected. If the install fails, please reboot and try again\\n" + fi +} + +install_dependent_packages() { + + # Install packages passed in via argument array + # No spinner - conflicts with set -e + declare -a installArray + + # Debian based package install - debconf will download the entire package list + # so we just create an array of packages not currently installed to cut down on the + # amount of download traffic. + # NOTE: We may be able to use this installArray in the future to create a list of package that were + # installed by us, and remove only the installed packages, and not the entire list. + if is_command apt-get ; then + # For each package, check if it's already installed (and if so, don't add it to the installArray) + for i in "$@"; do + printf " %b Checking for %s..." "${INFO}" "${i}" + if dpkg-query -W -f='${Status}' "${i}" 2>/dev/null | grep "ok installed" &> /dev/null; then + printf "%b %b Checking for %s\\n" "${OVER}" "${TICK}" "${i}" + else + printf "%b %b Checking for %s (will be installed)\\n" "${OVER}" "${INFO}" "${i}" + installArray+=("${i}") + fi + done + # If there's anything to install, install everything in the list. + if [[ "${#installArray[@]}" -gt 0 ]]; then + test_dpkg_lock + # Running apt-get install with minimal output can cause some issues with + # requiring user input (e.g password for phpmyadmin see #218) + printf " %b Processing %s install(s) for: %s, please wait...\\n" "${INFO}" "${PKG_MANAGER}" "${installArray[*]}" + printf '%*s\n' "${c}" '' | tr " " -; + "${PKG_INSTALL[@]}" "${installArray[@]}" + printf '%*s\n' "${c}" '' | tr " " -; + return + fi + printf "\\n" + return 0 + fi + + # Install Fedora/CentOS packages + for i in "$@"; do + # For each package, check if it's already installed (and if so, don't add it to the installArray) + printf " %b Checking for %s..." "${INFO}" "${i}" + if "${PKG_MANAGER}" -q list installed "${i}" &> /dev/null; then + printf "%b %b Checking for %s\\n" "${OVER}" "${TICK}" "${i}" + else + printf "%b %b Checking for %s (will be installed)\\n" "${OVER}" "${INFO}" "${i}" + installArray+=("${i}") + fi + done + # If there's anything to install, install everything in the list. + if [[ "${#installArray[@]}" -gt 0 ]]; then + printf " %b Processing %s install(s) for: %s, please wait...\\n" "${INFO}" "${PKG_MANAGER}" "${installArray[*]}" + printf '%*s\n' "${c}" '' | tr " " -; + "${PKG_INSTALL[@]}" "${installArray[@]}" + printf '%*s\n' "${c}" '' | tr " " -; + return + fi + printf "\\n" + return 0 +} + +# Install the Web interface dashboard +installPiholeWeb() { + printf "\\n %b Installing 404 page...\\n" "${INFO}" + + local str="Creating directory for 404 page, and copying files" + printf " %b %s..." "${INFO}" "${str}" + # Install the directory + install -d -m 0755 ${PI_HOLE_404_DIR} + # and the 404 handler + install -D -m 644 ${PI_HOLE_LOCAL_REPO}/advanced/index.php ${PI_HOLE_404_DIR}/ + + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + + local str="Backing up index.lighttpd.html" + printf " %b %s..." "${INFO}" "${str}" + # If the default index file exists, + if [[ -f "${webroot}/index.lighttpd.html" ]]; then + # back it up + mv ${webroot}/index.lighttpd.html ${webroot}/index.lighttpd.orig + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + # Otherwise, don't do anything + printf "%b %b %s\\n" "${OVER}" "${INFO}" "${str}" + printf " No default index.lighttpd.html file found... not backing up\\n" + fi + + # Install Sudoers file + local str="Installing sudoer file" + printf "\\n %b %s..." "${INFO}" "${str}" + # Make the .d directory if it doesn't exist, + install -d -m 755 /etc/sudoers.d/ + # and copy in the pihole sudoers file + install -m 0640 ${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole.sudo /etc/sudoers.d/pihole + # Add lighttpd user (OS dependent) to sudoers file + echo "${LIGHTTPD_USER} ALL=NOPASSWD: ${PI_HOLE_BIN_DIR}/pihole" >> /etc/sudoers.d/pihole + + # If the Web server user is lighttpd, + if [[ "$LIGHTTPD_USER" == "lighttpd" ]]; then + # Allow executing pihole via sudo with Fedora + # Usually /usr/local/bin ${PI_HOLE_BIN_DIR} is not permitted as directory for sudoable programs + echo "Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:${PI_HOLE_BIN_DIR}" >> /etc/sudoers.d/pihole + fi + # Set the strict permissions on the file + chmod 0440 /etc/sudoers.d/pihole + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" +} + +# Installs a cron file +installCron() { + # Install the cron job + local str="Installing latest Cron script" + printf "\\n %b %s..." "${INFO}" "${str}" + # Copy the cron file over from the local repo + # File must not be world or group writeable and must be owned by root + install -D -m 644 -T -o root -g root ${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole.cron /etc/cron.d/pihole + # Randomize gravity update time + sed -i "s/59 1 /$((1 + RANDOM % 58)) $((3 + RANDOM % 2))/" /etc/cron.d/pihole + # Randomize update checker time + sed -i "s/59 17/$((1 + RANDOM % 58)) $((12 + RANDOM % 8))/" /etc/cron.d/pihole + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" +} + +# Gravity is a very important script as it aggregates all of the domains into a single HOSTS formatted list, +# which is what Pi-hole needs to begin blocking ads +runGravity() { + # Run gravity in the current shell + { /opt/pihole/gravity.sh --force; } +} + +# Check if the pihole user exists and create if it does not +create_pihole_user() { + local str="Checking for user 'pihole'" + printf " %b %s..." "${INFO}" "${str}" + # If the pihole user exists, + if id -u pihole &> /dev/null; then + # and if the pihole group exists, + if getent group pihole > /dev/null 2>&1; then + # succeed + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + local str="Checking for group 'pihole'" + printf " %b %s..." "${INFO}" "${str}" + local str="Creating group 'pihole'" + # if group can be created + if groupadd pihole; then + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + local str="Adding user 'pihole' to group 'pihole'" + printf " %b %s..." "${INFO}" "${str}" + # if pihole user can be added to group pihole + if usermod -g pihole pihole; then + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + fi + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + fi + fi + else + # If the pihole user doesn't exist, + printf "%b %b %s" "${OVER}" "${CROSS}" "${str}" + local str="Creating user 'pihole'" + printf "%b %b %s..." "${OVER}" "${INFO}" "${str}" + # create her with the useradd command, + if getent group pihole > /dev/null 2>&1; then + # then add her to the pihole group (as it already exists) + if useradd -r --no-user-group -g pihole -s /usr/sbin/nologin pihole; then + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + fi + else + # add user pihole with default group settings + if useradd -r -s /usr/sbin/nologin pihole; then + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + else + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + fi + fi + fi +} + +# This function saves any changes to the setup variables into the setupvars.conf file for future runs +finalExports() { + # If the setup variable file exists, + if [[ -e "${setupVars}" ]]; then + # update the variables in the file + sed -i.update.bak '/PIHOLE_INTERFACE/d;/PIHOLE_DNS_1\b/d;/PIHOLE_DNS_2\b/d;/QUERY_LOGGING/d;/INSTALL_WEB_SERVER/d;/INSTALL_WEB_INTERFACE/d;/LIGHTTPD_ENABLED/d;/CACHE_SIZE/d;/DNS_FQDN_REQUIRED/d;/DNS_BOGUS_PRIV/d;/DNSMASQ_LISTENING/d;' "${setupVars}" + fi + # echo the information to the user + { + echo "PIHOLE_INTERFACE=${PIHOLE_INTERFACE}" + echo "PIHOLE_DNS_1=${PIHOLE_DNS_1}" + echo "PIHOLE_DNS_2=${PIHOLE_DNS_2}" + echo "QUERY_LOGGING=${QUERY_LOGGING}" + echo "INSTALL_WEB_SERVER=${INSTALL_WEB_SERVER}" + echo "INSTALL_WEB_INTERFACE=${INSTALL_WEB_INTERFACE}" + echo "LIGHTTPD_ENABLED=${LIGHTTPD_ENABLED}" + echo "CACHE_SIZE=${CACHE_SIZE}" + echo "DNS_FQDN_REQUIRED=${DNS_FQDN_REQUIRED:-true}" + echo "DNS_BOGUS_PRIV=${DNS_BOGUS_PRIV:-true}" + echo "DNSMASQ_LISTENING=${DNSMASQ_LISTENING:-local}" + }>> "${setupVars}" + chmod 644 "${setupVars}" + + # Set the privacy level + sed -i '/PRIVACYLEVEL/d' "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" + echo "PRIVACYLEVEL=${PRIVACY_LEVEL}" >> "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" + + # Bring in the current settings and the functions to manipulate them + source "${setupVars}" + # shellcheck source=advanced/Scripts/webpage.sh + source "${PI_HOLE_LOCAL_REPO}/advanced/Scripts/webpage.sh" + + # Look for DNS server settings which would have to be reapplied + ProcessDNSSettings + + # Look for DHCP server settings which would have to be reapplied + ProcessDHCPSettings +} + +# Install the logrotate script +installLogrotate() { + local str="Installing latest logrotate script" + local target=/etc/pihole/logrotate + + printf "\\n %b %s..." "${INFO}" "${str}" + if [[ -f ${target} ]]; then + + # Account for changed logfile paths from /var/log -> /var/log/pihole/ made in core v5.11. + if grep -q "/var/log/pihole.log" ${target} || grep -q "/var/log/pihole-FTL.log" ${target}; then + sed -i 's/\/var\/log\/pihole.log/\/var\/log\/pihole\/pihole.log/g' ${target} + sed -i 's/\/var\/log\/pihole-FTL.log/\/var\/log\/pihole\/FTL.log/g' ${target} + + printf "\\n\\t%b Old log file paths updated in existing logrotate file. \\n" "${INFO}" + return 3 + fi + + printf "\\n\\t%b Existing logrotate file found. No changes made.\\n" "${INFO}" + # Return value isn't that important, using 2 to indicate that it's not a fatal error but + # the function did not complete. + return 2 + fi + # Copy the file over from the local repo + install -D -m 644 -T "${PI_HOLE_LOCAL_REPO}"/advanced/Templates/logrotate ${target} + # 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 there is a usergroup for log rotation, + if [[ -n "${logusergroup}" ]]; then + # replace the line in the logrotate script with that usergroup. + sed -i "s/# su #/su ${logusergroup}/g;" ${target} + fi + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" +} + +# Install base files and web interface +installPihole() { + # If the user wants to install the Web interface, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + if [[ ! -d "${webroot}" ]]; then + # make the Web directory if necessary + install -d -m 0755 ${webroot} + fi + + if [[ "${INSTALL_WEB_SERVER}" == true ]]; then + # Set the owner and permissions + chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} ${webroot} + chmod 0775 ${webroot} + # Repair permissions if webroot is not world readable + chmod a+rx /var/www + chmod a+rx ${webroot} + # Give lighttpd access to the pihole group so the web interface can + # manage the gravity.db database + usermod -a -G pihole ${LIGHTTPD_USER} + # If the lighttpd command is executable, + if is_command lighty-enable-mod ; then + # enable fastcgi and fastcgi-php + lighty-enable-mod fastcgi fastcgi-php > /dev/null || true + else + # Otherwise, show info about installing them + printf " %b Warning: 'lighty-enable-mod' utility not found\\n" "${INFO}" + printf " Please ensure fastcgi is enabled if you experience issues\\n" + fi + fi + fi + # Install base files and web interface + if ! installScripts; then + printf " %b Failure in dependent script copy function.\\n" "${CROSS}" + exit 1 + fi + # Install config files + if ! installConfigs; then + printf " %b Failure in dependent config copy function.\\n" "${CROSS}" + exit 1 + fi + # If the user wants to install the dashboard, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + # do so + installPiholeWeb + fi + # Install the cron file + installCron + + # Install the logrotate file + installLogrotate || true + + # Check if dnsmasq is present. If so, disable it and back up any possible + # config file + disable_dnsmasq + + # install a man page entry for pihole + install_manpage + + # Update setupvars.conf with any variables that may or may not have been changed during the install + finalExports +} + +# SELinux +checkSelinux() { + local DEFAULT_SELINUX + local CURRENT_SELINUX + local SELINUX_ENFORCING=0 + # Check for SELinux configuration file and getenforce command + if [[ -f /etc/selinux/config ]] && is_command getenforce; then + # Check the default SELinux mode + DEFAULT_SELINUX=$(awk -F= '/^SELINUX=/ {print $2}' /etc/selinux/config) + case "${DEFAULT_SELINUX,,}" in + enforcing) + printf " %b %bDefault SELinux: %s%b\\n" "${CROSS}" "${COL_RED}" "${DEFAULT_SELINUX,,}" "${COL_NC}" + SELINUX_ENFORCING=1 + ;; + *) # 'permissive' and 'disabled' + printf " %b %bDefault SELinux: %s%b\\n" "${TICK}" "${COL_GREEN}" "${DEFAULT_SELINUX,,}" "${COL_NC}" + ;; + esac + # Check the current state of SELinux + CURRENT_SELINUX=$(getenforce) + case "${CURRENT_SELINUX,,}" in + enforcing) + printf " %b %bCurrent SELinux: %s%b\\n" "${CROSS}" "${COL_RED}" "${CURRENT_SELINUX,,}" "${COL_NC}" + SELINUX_ENFORCING=1 + ;; + *) # 'permissive' and 'disabled' + printf " %b %bCurrent SELinux: %s%b\\n" "${TICK}" "${COL_GREEN}" "${CURRENT_SELINUX,,}" "${COL_NC}" + ;; + esac + else + echo -e " ${INFO} ${COL_GREEN}SELinux not detected${COL_NC}"; + fi + # Exit the installer if any SELinux checks toggled the flag + if [[ "${SELINUX_ENFORCING}" -eq 1 ]] && [[ -z "${PIHOLE_SELINUX}" ]]; then + printf " Pi-hole does not provide an SELinux policy as the required changes modify the security of your system.\\n" + printf " Please refer to https://wiki.centos.org/HowTos/SELinux if SELinux is required for your deployment.\\n" + printf " This check can be skipped by setting the environment variable %bPIHOLE_SELINUX%b to %btrue%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " e.g: export PIHOLE_SELINUX=true\\n" + printf " By setting this variable to true you acknowledge there may be issues with Pi-hole during or after the install\\n" + printf "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; + exit 1; + elif [[ "${SELINUX_ENFORCING}" -eq 1 ]] && [[ -n "${PIHOLE_SELINUX}" ]]; then + printf " %b %bSELinux Enforcing detected%b. PIHOLE_SELINUX env variable set - installer will continue\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}" + fi +} + +# Installation complete message with instructions for the user +displayFinalMessage() { + # If the number of arguments is > 0, + if [[ "${#1}" -gt 0 ]] ; then + # set the password to the first argument. + pwstring="$1" + elif [[ $(grep 'WEBPASSWORD' -c "${setupVars}") -gt 0 ]]; then + # Else if the password exists from previous setup, we'll load it later + pwstring="unchanged" + else + # Else, inform the user that there is no set password. + pwstring="NOT SET" + fi + # If the user wants to install the dashboard, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + # Store a message in a variable and display it + additional="View the web interface at http://pi.hole/admin or http://${IPV4_ADDRESS%/*}/admin\\n\\nYour Admin Webpage login password is ${pwstring}" + fi + + # Final completion message to user + dialog --no-shadow --keep-tite \ + --title "Installation Complete!" \ + --msgbox "Configure your devices to use the Pi-hole as their DNS server using:\ +\\n\\nIPv4: ${IPV4_ADDRESS%/*}\ +\\nIPv6: ${IPV6_ADDRESS:-"Not Configured"}\ +\\nIf you have not done so already, the above IP should be set to static.\ +\\n${additional}" "${r}" "${c}" +} + +update_dialogs() { + # If pihole -r "reconfigure" option was selected, + if [[ "${reconfigure}" = true ]]; then + # set some variables that will be used + opt1a="Repair" + opt1b="This will retain existing settings" + strAdd="You will remain on the same version" + else + # Otherwise, set some variables with different values + opt1a="Update" + opt1b="This will retain existing settings." + strAdd="You will be updated to the latest version." + fi + opt2a="Reconfigure" + opt2b="Resets Pi-hole and allows re-selecting settings." + + # Display the information to the user + UpdateCmd=$(dialog --no-shadow --keep-tite --output-fd 1 \ + --cancel-label Exit \ + --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}" || true) + + result=$? + case ${result} in + "${DIALOG_CANCEL}" | "${DIALOG_ESC}") + printf " %b Cancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}" + exit 1 + ;; + esac + + # Set the variable based on if the user chooses + case ${UpdateCmd} in + # repair, or + "${opt1a}") + printf " %b %s option selected\\n" "${INFO}" "${opt1a}" + useUpdateVars=true + ;; + # reconfigure, + "${opt2a}") + printf " %b %s option selected\\n" "${INFO}" "${opt2a}" + useUpdateVars=false + ;; + esac +} + +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 +} + +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 ls-remote --heads --quiet | cut -d'/' -f3- -; } 2>&1 ) + # echo status for calling function to capture + 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}'" + printf " %b %s" "${INFO}" "$str" + git checkout "${branch}" --quiet || return 1 + printf "%b %b %s\\n" "${OVER}" "${TICK}" "$str" + # Data in the repositories is public anyway so we can make it readable by everyone (+r to keep executable permission if already set by git) + chmod -R a+rX "${directory}" + + git_pull=$(git pull --no-rebase || return 1) + + printf " %b %s\\n" "${INFO}" "${git_pull}" + + return 0 +} + +clone_or_update_repos() { + # If the user wants to reconfigure, + if [[ "${reconfigure}" == true ]]; then + printf " %b Performing reconfiguration, skipping download of local repos\\n" "${INFO}" + # Reset the Core repo + resetRepo ${PI_HOLE_LOCAL_REPO} || \ + { printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"; \ + exit 1; \ + } + # If the Web interface was installed, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + # reset it's repo + resetRepo ${webInterfaceDir} || \ + { printf " %b Unable to reset %s, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceDir}" "${COL_NC}"; \ + exit 1; \ + } + fi + # Otherwise, a repair is happening + else + # so get git files for Core + getGitFiles ${PI_HOLE_LOCAL_REPO} ${piholeGitUrl} || \ + { printf " %b Unable to clone %s into %s, unable to continue%b\\n" "${COL_LIGHT_RED}" "${piholeGitUrl}" "${PI_HOLE_LOCAL_REPO}" "${COL_NC}"; \ + exit 1; \ + } + # If the Web interface was installed, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + # get the Web git files + getGitFiles ${webInterfaceDir} ${webInterfaceGitUrl} || \ + { printf " %b Unable to clone %s into ${webInterfaceDir}, exiting installer%b\\n" "${COL_LIGHT_RED}" "${webInterfaceGitUrl}" "${COL_NC}"; \ + exit 1; \ + } + fi + fi +} + +# Download FTL binary to random temp directory and install FTL binary +# Disable directive for SC2120 a value _can_ be passed to this function, but it is passed from an external script that sources this one +# shellcheck disable=SC2120 +FTLinstall() { + # Local, named variables + local str="Android FTL Install " + printf " %b %s..." "${INFO}" "${str}" + + # Move into the temp ftl directory + pushd "$(mktemp -d)" > /dev/null || { printf "Unable to make temporary directory for FTL binary download\\n"; return 1; } + + local ftlBranch + local url + + if [[ -f "/etc/pihole/ftlbranch" ]];then + ftlBranch=$(> ${installLogLoc} ; cd .. + + if cp ./FTL/pihole-FTL "${binary}"; then + # If the compile worked, get sha1 of the binary we just built. + sha1sum "${binary}" > "${binary}.sha1" + + # If we downloaded binary file (as opposed to text), + if sha1sum --status --quiet -c "${binary}".sha1; then + printf "transferred... " + + # Before stopping FTL, we download the macvendor database + curl -sSL "https://ftl.pi-hole.net/macvendor.db" -o "${PI_HOLE_CONFIG_DIR}/macvendor.db" || true + + # Stop pihole-FTL service if available + stop_service pihole-FTL &> /dev/null + + # Install the new version with the correct permissions + install -T -m 0755 "${binary}" /usr/bin/pihole-FTL + + # Move back into the original directory the user was in + popd > /dev/null || { printf "Unable to return to original directory after FTL binary download.\\n"; return 1; } + + # Installed the FTL service + printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" + return 0 + else + # Otherwise, the hash download failed, so print and exit. + popd > /dev/null || { printf "Unable to return to original directory after FTL binary download.\\n"; return 1; } + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + printf " %b Error: Download of %s/%s failed (checksum error)%b\\n" "${COL_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}" + return 1 + fi + else + # Otherwise, the download failed, so print and exit. + popd > /dev/null || { printf "Unable to return to original directory after FTL binary download.\\n"; return 1; } + printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + # The URL could not be found + printf " %b Error: URL %s/%s not found%b\\n" "${COL_LIGHT_RED}" "${url}" "${binary}" "${COL_NC}" + return 1 + fi +} + +disable_dnsmasq() { + # dnsmasq can now be stopped and disabled if it exists + if is_command dnsmasq; then + if check_service_active "dnsmasq";then + printf " %b FTL can now resolve DNS Queries without dnsmasq running separately\\n" "${INFO}" + stop_service dnsmasq + disable_service dnsmasq + fi + fi + + # Backup existing /etc/dnsmasq.conf if present and ensure that + # /etc/dnsmasq.conf contains only "conf-dir=/etc/dnsmasq.d" + local conffile="/etc/dnsmasq.conf" + if [[ -f "${conffile}" ]]; then + printf " %b Backing up %s to %s.old\\n" "${INFO}" "${conffile}" "${conffile}" + mv "${conffile}" "${conffile}.old" + fi + # Create /etc/dnsmasq.conf + echo "conf-dir=/etc/dnsmasq.d" > "${conffile}" + chmod 644 "${conffile}" +} + +get_binary_name() { + # This gives the machine architecture which may be different from the OS architecture... + local machine + machine=$(uname -m) + + local l_binary + + local str="Detecting processor" + printf " %b %s..." "${INFO}" "${str}" + # If the machine is arm or aarch + if [[ "${machine}" == "arm"* || "${machine}" == *"aarch"* ]]; then + # ARM + local rev + rev=$(uname -m | sed "s/[^0-9]//g;") + local lib + lib=$(ldd "$(which sh)" | grep -E '^\s*/lib' | awk '{ print $1 }') + if [[ "${lib}" == "/lib/ld-linux-aarch64.so.1" ]]; then + printf "%b %b Detected AArch64 (64 Bit ARM) processor\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-aarch64-linux-gnu" + elif [[ "${lib}" == "/lib/ld-linux-armhf.so.3" ]]; then + # Hard-float available: Use gnueabihf binaries + # If ARMv8 or higher is found (e.g., BCM2837 as found in Raspberry Pi Model 3B) + if [[ "${rev}" -gt 7 ]]; then + printf "%b %b Detected ARMv8 (or newer) processor\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-armv8-linux-gnueabihf" + elif [[ "${rev}" -eq 7 ]]; then + # Otherwise, if ARMv7 is found (e.g., BCM2836 as found in Raspberry Pi Model 2) + printf "%b %b Detected ARMv7 processor (with hard-float support)\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-armv7-linux-gnueabihf" + else + # Otherwise, use the ARMv6 binary (e.g., BCM2835 as found in Raspberry Pi Zero and Model 1) + printf "%b %b Detected ARMv6 processor (with hard-float support)\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-armv6-linux-gnueabihf" + fi + else + # No hard-float support found: Use gnueabi binaries + # Use the ARMv4-compliant binary only if we detected an ARMv4T core + if [[ "${rev}" -eq 4 ]]; then + printf "%b %b Detected ARMv4 processor\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-armv4-linux-gnueabi" + # Otherwise, use the ARMv5 binary. To date (end of 2020), all modern ARM processors + # are backwards-compatible to the ARMv5 + else + printf "%b %b Detected ARMv5 (or newer) processor\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-armv5-linux-gnueabi" + fi + fi + elif [[ "${machine}" == "x86_64" ]]; then + # This gives the processor of packages dpkg installs (for example, "i386") + local dpkgarch + dpkgarch=$(dpkg --print-processor 2> /dev/null || dpkg --print-architecture 2> /dev/null) + + # Special case: This is a 32 bit OS, installed on a 64 bit machine + # -> change machine processor to download the 32 bit executable + # We only check this for Debian-based systems as this has been an issue + # in the past (see https://github.com/pi-hole/pi-hole/pull/2004) + if [[ "${dpkgarch}" == "i386" ]]; then + printf "%b %b Detected 32bit (i686) processor\\n" "${OVER}" "${TICK}" + l_binary="pihole-FTL-linux-x86_32" + else + # 64bit + printf "%b %b Detected x86_64 processor\\n" "${OVER}" "${TICK}" + # set the binary to be used + l_binary="pihole-FTL-linux-x86_64" + fi + else + # Something else - we try to use 32bit executable and warn the user + if [[ ! "${machine}" == "i686" ]]; then + printf "%b %b %s...\\n" "${OVER}" "${CROSS}" "${str}" + printf " %b %bNot able to detect processor (unknown: %s), trying x86 (32bit) executable%b\\n" "${INFO}" "${COL_LIGHT_RED}" "${machine}" "${COL_NC}" + printf " %b Contact Pi-hole Support if you experience issues (e.g: FTL not running)\\n" "${INFO}" + else + printf "%b %b Detected 32bit (i686) processor\\n" "${OVER}" "${TICK}" + fi + l_binary="pihole-FTL-linux-x86_32" + fi + + # Returning a string value via echo + echo ${l_binary} +} + +FTLcheckUpdate() { + #In the next section we check to see if FTL is already installed (in case of pihole -r). + #If the installed version matches the latest version, then check the installed sha1sum of the binary vs the remote sha1sum. If they do not match, then download + printf " %b Checking for existing FTL binary...\\n" "${INFO}" + + local ftlLoc + ftlLoc=$(command -v pihole-FTL 2>/dev/null) + + local ftlBranch + + if [[ -f "/etc/pihole/ftlbranch" ]];then + ftlBranch=$("$TEMPLOG" + # Delete templog, but allow for addressing via file handle + # This lets us write to the log without having a temporary file on the drive, which + # is meant to be a security measure so there is not a lingering file on the drive during the install process + rm "$TEMPLOG" +} + +copy_to_install_log() { + # Copy the contents of file descriptor 3 into the install log + # Since we use color codes such as '\e[1;33m', they should be removed + sed 's/\[[0-9;]\{1,5\}m//g' < /proc/$$/fd/3 > "${installLogLoc}" + chmod 644 "${installLogLoc}" +} + +main() { + ######## FIRST CHECK ######## + # Must be root to install + local str="Root user check" + printf "\\n" + + # If the user's id is zero, + if [[ "${EUID}" -eq 0 ]]; then + # they are root and all is good + printf " %b %s\\n" "${TICK}" "${str}" + # Show the Pi-hole logo so people know it's genuine since the logo and name are trademarked + show_ascii_berry + make_temporary_log + else + # Otherwise, they do not have enough privileges, so let the user know + printf " %b %s\\n" "${INFO}" "${str}" + printf " %b %bScript called with non-root privileges%b\\n" "${INFO}" "${COL_LIGHT_RED}" "${COL_NC}" + printf " The Pi-hole requires elevated privileges to install and run\\n" + printf " Please check the installer for any concerns regarding this requirement\\n" + printf " Make sure to download this script from a trusted source\\n\\n" + printf " %b Sudo utility check" "${INFO}" + + # If the sudo command exists, try rerunning as admin + if is_command sudo ; then + printf "%b %b Sudo utility check\\n" "${OVER}" "${TICK}" + + # when run via curl piping + if [[ "$0" == "bash" ]]; then + # Download the install script and run it with admin rights + exec curl -sSL https://raw.githubusercontent.com/pi-hole/pi-hole/master/automated%20install/basic-install.sh | sudo bash "$@" + else + # when run via calling local bash script + exec sudo bash "$0" "$@" + fi + + exit $? + else + # Otherwise, tell the user they need to run the script as root, and bail + printf "%b %b Sudo utility check\\n" "${OVER}" "${CROSS}" + printf " %b Sudo is needed for the Web Interface to run pihole commands\\n\\n" "${INFO}" + printf " %b %bPlease re-run this installer as root${COL_NC}\\n" "${INFO}" "${COL_LIGHT_RED}" + exit 1 + fi + fi + + # Check if SELinux is Enforcing and exit before doing anything else + checkSelinux + + # Check for supported package managers so that we may install dependencies + package_manager_detect + + # Notify user of package availability + notify_package_updates_available + + # Install packages necessary to perform os_check + printf " %b Checking for / installing Required dependencies for OS Check...\\n" "${INFO}" + install_dependent_packages "${OS_CHECK_DEPS[@]}" + + # Check that the installed OS is officially supported - display warning if not + os_check + + # Install packages used by this installation script + printf " %b Checking for / installing Required dependencies for this install script...\\n" "${INFO}" + install_dependent_packages "${INSTALLER_DEPS[@]}" + + # If the setup variable file exists, + if [[ -f "${setupVars}" ]]; then + # if it's running unattended, + if [[ "${runUnattended}" == true ]]; then + printf " %b Performing unattended setup, no dialogs will be displayed\\n" "${INFO}" + # Use the setup variables + useUpdateVars=true + # also disable debconf-apt-progress dialogs + export DEBIAN_FRONTEND="noninteractive" + else + # If running attended, show the available options (repair/reconfigure) + update_dialogs + fi + fi + + if [[ "${useUpdateVars}" == false ]]; then + # Display welcome dialogs + welcomeDialogs + # Create directory for Pi-hole storage + install -d -m 755 /etc/pihole/ + # Determine available interfaces + get_available_interfaces + # Find interfaces and let the user choose one + chooseInterface + # find IPv4 and IPv6 information of the device + collect_v4andv6_information + # Decide what upstream DNS Servers to use + setDNS + # Give the user a choice of blocklists to include in their install. Or not. + chooseBlocklists + # Let the user decide if they want the web interface to be installed automatically + setAdminFlag + # Let the user decide if they want query logging enabled... + setLogging + # Let the user decide the FTL privacy level + setPrivacyLevel + else + # Setup adlist file if not exists + installDefaultBlocklists + + # Source ${setupVars} to use predefined user variables in the functions + source "${setupVars}" + + # Get the privacy level if it exists (default is 0) + if [[ -f "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" ]]; then + PRIVACY_LEVEL=$(sed -ne 's/PRIVACYLEVEL=\(.*\)/\1/p' "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf") + + # If no setting was found, default to 0 + PRIVACY_LEVEL="${PRIVACY_LEVEL:-0}" + fi + fi + # Download or update the scripts by updating the appropriate git repos + clone_or_update_repos + + # Install the Core dependencies + local dep_install_list=("${PIHOLE_DEPS[@]}") + if [[ "${INSTALL_WEB_SERVER}" == true ]]; then + # And, if the setting says so, install the Web admin interface dependencies + dep_install_list+=("${PIHOLE_WEB_DEPS[@]}") + fi + + # Install packages used by the actual software + printf " %b Checking for / installing Required dependencies for Pi-hole software...\\n" "${INFO}" + install_dependent_packages "${dep_install_list[@]}" + unset dep_install_list + + # On some systems, lighttpd is not enabled on first install. We need to enable it here if the user + # has chosen to install the web interface, else the LIGHTTPD_ENABLED check will fail + if [[ "${INSTALL_WEB_SERVER}" == true ]]; then + enable_service lighttpd + fi + # Determine if lighttpd is correctly enabled + if check_service_active "lighttpd"; then + LIGHTTPD_ENABLED=true + else + LIGHTTPD_ENABLED=false + fi + # Create the pihole user + create_pihole_user + + # Check if FTL is installed - do this early on as FTL is a hard dependency for Pi-hole + local funcOutput + funcOutput=$(get_binary_name) #Store output of get_binary_name here + local binary + binary="pihole-FTL${funcOutput##*pihole-FTL}" #binary name will be the last line of the output of get_binary_name (it always begins with pihole-FTL) + local theRest + theRest="${funcOutput%pihole-FTL*}" # Print the rest of get_binary_name's output to display (cut out from first instance of "pihole-FTL") + if ! FTLdetect "${binary}" "${theRest}"; then + printf " %b FTL Engine not installed\\n" "${CROSS}" + exit 1 + fi + + # Install and log everything to a file + installPihole | tee -a /proc/$$/fd/3 + + # Copy the temp log file into final log location for storage + copy_to_install_log + + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + # Add password to web UI if there is none + pw="" + # If no password is set, + if [[ $(grep 'WEBPASSWORD' -c "${setupVars}") == 0 ]] ; then + # generate a random password + pw=$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c 8) + # shellcheck disable=SC1091 + . /opt/pihole/webpage.sh + echo "WEBPASSWORD=$(HashPassword "${pw}")" >> "${setupVars}" + fi + fi + + # Check for and disable systemd-resolved-DNSStubListener before reloading resolved + # DNSStubListener needs to remain in place for installer to download needed files, + # so this change needs to be made after installation is complete, + # but before starting or resarting the dnsmasq or ftl services + disable_resolved_stublistener + + # If the Web server was installed, + if [[ "${INSTALL_WEB_SERVER}" == true ]]; then + if [[ "${LIGHTTPD_ENABLED}" == true ]]; then + restart_service lighttpd + enable_service lighttpd + else + printf " %b Lighttpd is disabled, skipping service restart\\n" "${INFO}" + fi + fi + + printf " %b Restarting services...\\n" "${INFO}" + # Start services + + # Enable FTL + # Ensure the service is enabled before trying to start it + # Fixes a problem reported on Ubuntu 18.04 where trying to start + # the service before enabling causes installer to exit + enable_service pihole-FTL + + # If this is an update from a previous Pi-hole installation + # we need to move any existing `pihole*` logs from `/var/log` to `/var/log/pihole` + # if /var/log/pihole.log is not a symlink (set during FTL startup) move the files + # can be removed with Pi-hole v6.0 + # To be sure FTL is not running when we move the files we explicitly stop it here + + stop_service pihole-FTL &> /dev/null + + if [ ! -d /var/log/pihole/ ]; then + mkdir -m 0755 /var/log/pihole/ + fi + + # Special handling for pihole-FTL.log -> pihole/FTL.log + if [ -f /var/log/pihole-FTL.log ] && [ ! -L /var/log/pihole-FTL.log ]; then + # /var/log/pihole-FTL.log -> /var/log/pihole/FTL.log + # /var/log/pihole-FTL.log.1 -> /var/log/pihole/FTL.log.1 + # /var/log/pihole-FTL.log.2.gz -> /var/log/pihole/FTL.log.2.gz + # /var/log/pihole-FTL.log.3.gz -> /var/log/pihole/FTL.log.3.gz + # /var/log/pihole-FTL.log.4.gz -> /var/log/pihole/FTL.log.4.gz + # /var/log/pihole-FTL.log.5.gz -> /var/log/pihole/FTL.log.5.gz + for f in /var/log/pihole-FTL.log*; do mv "$f" "$( sed "s/pihole-/pihole\//" <<< "$f")"; done + fi + + # Remaining log files + if [ -f /var/log/pihole.log ] && [ ! -L /var/log/pihole.log ]; then + mv /var/log/pihole*.* /var/log/pihole/ 2>/dev/null + fi + + restart_service pihole-FTL + + # Download and compile the aggregated block list + runGravity + + # Force an update of the updatechecker + /opt/pihole/updatecheck.sh + /opt/pihole/updatecheck.sh x remote + + if [[ "${useUpdateVars}" == false ]]; then + displayFinalMessage "${pw}" + fi + + # If the Web interface was installed, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + # If there is a password, + if (( ${#pw} > 0 )) ; then + # display the password + printf " %b Web Interface password: %b%s%b\\n" "${INFO}" "${COL_LIGHT_GREEN}" "${pw}" "${COL_NC}" + printf " %b This can be changed using 'pihole -a -p'\\n\\n" "${INFO}" + fi + fi + + if [[ "${useUpdateVars}" == false ]]; then + # If the Web interface was installed, + if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then + printf " %b View the web interface at http://pi.hole/admin or http://%s/admin\\n\\n" "${INFO}" "${IPV4_ADDRESS%/*}" + fi + # Explain to the user how to use Pi-hole as their DNS server + printf " %b You may now configure your devices to use the Pi-hole as their DNS server\\n" "${INFO}" + [[ -n "${IPV4_ADDRESS%/*}" ]] && printf " %b Pi-hole DNS (IPv4): %s\\n" "${INFO}" "${IPV4_ADDRESS%/*}" + [[ -n "${IPV6_ADDRESS}" ]] && printf " %b Pi-hole DNS (IPv6): %s\\n" "${INFO}" "${IPV6_ADDRESS}" + printf " %b If you have not done so already, the above IP should be set to static.\\n" "${INFO}" + INSTALL_TYPE="Installation" + else + INSTALL_TYPE="Update" + fi + + # Display where the log file is + printf "\\n %b The install log is located at: %s\\n" "${INFO}" "${installLogLoc}" + printf " %b %b%s complete! %b\\n" "${TICK}" "${COL_LIGHT_GREEN}" "${INSTALL_TYPE}" "${COL_NC}" + + if [[ "${INSTALL_TYPE}" == "Update" ]]; then + printf "\\n" + "${PI_HOLE_BIN_DIR}"/pihole version --current + fi +} + +# allow to source this script without running it +if [[ "${SKIP_INSTALL}" != true ]] ; then + main "$@" +fi