#!/usr/bin/env bash # shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements # (c) 2017-2018 Pi-hole, LLC (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 # # Install with this command (from your Linux machine): # # curl -sSL https://install.pi-hole.net | bash # -e option instructs bash to immediately exit if any command [1] has a non-zero exit status # We do not want users to end up with a partially working install, so we exit the script # instead of continuing the installation with something broken set -e ######## VARIABLES ######### # For better maintainability, we store as much information that can change in variables # This allows us to make a change in one place that can 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 # 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 # shellcheck disable=SC2034 lighttpdConfig=/etc/lighttpd/lighttpd.conf # This is a file used for the colorized output coltable=/opt/pihole/COL_TABLE # We store several other folders and webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git" webInterfaceDir="/var/www/html/admin" piholeGitUrl="https://github.com/pi-hole/pi-hole.git" PI_HOLE_LOCAL_REPO="/etc/.pihole" # These are the names of pi-holes files, stored in an array PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage) # This folder is where the Pi-hole scripts will be installed PI_HOLE_INSTALL_DIR="/opt/pihole" useUpdateVars=false 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="" IPV6_ADDRESS="" # By default, query logging is enabled and the dashboard is set to be installed QUERY_LOGGING=true INSTALL_WEB_INTERFACE=true if [ -z "${USER}" ]; then USER="$(id -un)" fi # Find the rows and columns will default to 80x24 if it can not be detected screen_size=$(stty size 2>/dev/null || echo 24 80) rows=$(echo "${screen_size}" | awk '{print $1}') columns=$(echo "${screen_size}" | awk '{print $2}') # Divide by two so the dialogs take up half of the screen, which looks nice. r=$(( rows / 2 )) c=$(( columns / 2 )) # Unless the screen is tiny r=$(( r < 20 ? 20 : r )) c=$(( c < 70 ? 70 : c )) ######## Undocumented Flags. Shhh ######## # These are undocumented flags; some of which we can use when repairing an installation # The runUnattended flag is one example of this skipSpaceCheck=false reconfigure=false runUnattended=false INSTALL_WEB_SERVER=true # Check arguments for the undocumented flags for var in "$@"; do case "$var" in "--reconfigure" ) reconfigure=true;; "--i_do_not_follow_recommendations" ) skipSpaceCheck=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} " } # Compatibility distro_check() { # If apt-get is installed, then we know it's part of the Debian family if command -v apt-get &> /dev/null; then # Set some global variables here # We don't set them earlier since the family might be Red Hat, so these values would be different PKG_MANAGER="apt-get" # A variable to store the command used to update the package cache UPDATE_PKG_CACHE="${PKG_MANAGER} update" # An array for something... PKG_INSTALL=(${PKG_MANAGER} --yes --no-install-recommends install) # grep -c will return 1 retVal on 0 matches, block this throwing the set -e with an OR TRUE PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" # Some distros vary slightly so these fixes for dependencies may apply # Debian 7 doesn't have iproute2 so if the dry run install is successful, if ${PKG_MANAGER} install --dry-run iproute2 > /dev/null 2>&1; then # we can install it iproute_pkg="iproute2" # Otherwise, else # use iproute iproute_pkg="iproute" fi # Check for and determine version number (major and minor) of current php install if command -v php &> /dev/null; then phpInsVersion="$(php -v | head -n1 | grep -Po '(?<=PHP )[^ ]+')" echo -e " ${INFO} Existing PHP installation detected : PHP version $phpInsVersion" phpInsMajor="$(echo "$phpInsVersion" | cut -d\. -f1)" phpInsMinor="$(echo "$phpInsVersion" | cut -d\. -f2)" # Is installed php version 7.0 or greater if [ "$(echo "$phpInsMajor.$phpInsMinor < 7.0" | bc )" == 0 ]; then phpInsNewer=true fi fi # Check if installed php is v 7.0, or newer to determine packages to install if [[ "$phpInsNewer" != true ]]; then # Prefer the php metapackage if it's there if ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1; then phpVer="php" # fall back on the php5 packages else phpVer="php5" fi else # Newer php is installed, its common, cgi & sqlite counterparts are deps phpVer="php$phpInsMajor.$phpInsMinor" fi # We also need the correct version for `php-sqlite` (which differs across distros) if ${PKG_MANAGER} install --dry-run ${phpVer}-sqlite3 > /dev/null 2>&1; then phpSqlite="sqlite3" else phpSqlite="sqlite" fi # Since our install script is so large, we need several other programs to successfully get a machine provisioned # These programs are stored in an array so they can be looped through later INSTALLER_DEPS=(apt-utils dialog debconf dhcpcd5 git ${iproute_pkg} whiptail) # Pi-hole itself has several dependencies that also need to be installed PIHOLE_DEPS=(bc cron curl dnsutils iputils-ping lsof netcat psmisc sudo unzip wget idn2 sqlite3 libcap2-bin dns-root-data resolvconf) # The Web dashboard has some that also need to be installed # It's useful to separate the two since our repos are also setup as "Core" code and "Web" code PIHOLE_WEB_DEPS=(lighttpd ${phpVer}-common ${phpVer}-cgi ${phpVer}-${phpSqlite}) # The Web server user, LIGHTTPD_USER="www-data" # group, LIGHTTPD_GROUP="www-data" # and config file LIGHTTPD_CFG="lighttpd.conf.debian" # A function to check... test_dpkg_lock() { # An iterator used for counting loop iterations i=0 # fuser is a program to show which processes use the named files, sockets, or filesystems # So while the command is true while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do # Wait half a second sleep 0.5 # and increase the iterator ((i=i+1)) done # Always return success, since we only return if there is no # lock (anymore) return 0 } # If apt-get is not found, check for rpm to see if it's a Red Hat family OS elif command -v rpm &> /dev/null; then # Then check if dnf or yum is the package manager if command -v dnf &> /dev/null; then PKG_MANAGER="dnf" else PKG_MANAGER="yum" fi # Fedora and family update cache on every PKG_INSTALL call, no need for a separate update. UPDATE_PKG_CACHE=":" PKG_INSTALL=(${PKG_MANAGER} install -y) PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l" INSTALLER_DEPS=(dialog git iproute net-tools newt procps-ng which) PIHOLE_DEPS=(bc bind-utils cronie curl findutils nmap-ncat sudo unzip wget libidn2 psmisc) PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php-common php-cli php-pdo) LIGHTTPD_USER="lighttpd" LIGHTTPD_GROUP="lighttpd" LIGHTTPD_CFG="lighttpd.conf.fedora" # If the host OS is Fedora, if grep -qi 'fedora' /etc/redhat-release; then # all required packages should be available by default with the latest fedora release : # continue # or if host OS is CentOS, elif grep -qi 'centos' /etc/redhat-release; then # Pi-Hole currently supports CentOS 7+ with PHP7+ SUPPORTED_CENTOS_VERSION=7 SUPPORTED_CENTOS_PHP_VERSION=7 # Check current CentOS major release version CURRENT_CENTOS_VERSION=$(rpm -q --queryformat '%{VERSION}' centos-release) # Check if CentOS version is supported if [[ $CURRENT_CENTOS_VERSION -lt $SUPPORTED_CENTOS_VERSION ]]; then echo -e " ${CROSS} CentOS $CURRENT_CENTOS_VERSION is not suported." echo -e " Please update to CentOS release $SUPPORTED_CENTOS_VERSION or later" # exit the installer exit fi # on CentOS we need to add the EPEL repository to gain access to Fedora packages EPEL_PKG="epel-release" rpm -q ${EPEL_PKG} &> /dev/null || rc=$? if [[ $rc -ne 0 ]]; then echo -e " ${INFO} Enabling EPEL package repository (https://fedoraproject.org/wiki/EPEL)" "${PKG_INSTALL[@]}" ${EPEL_PKG} &> /dev/null echo -e " ${TICK} Installed ${EPEL_PKG}" fi # The default php on CentOS 7.x is 5.4 which is EOL # Check if the version of PHP available via installed repositories is >= to PHP 7 AVAILABLE_PHP_VERSION=$(${PKG_MANAGER} info php | grep -i version | grep -o '[0-9]\+' | head -1) if [[ $AVAILABLE_PHP_VERSION -ge $SUPPORTED_CENTOS_PHP_VERSION ]]; then # Since PHP 7 is available by default, install via default PHP package names : # do nothing as PHP is current else REMI_PKG="remi-release" REMI_REPO="remi-php72" rpm -q ${REMI_PKG} &> /dev/null || rc=$? if [[ $rc -ne 0 ]]; then # The PHP version available via default repositories is older than version 7 if ! whiptail --defaultno --title "PHP 7 Update (recommended)" --yesno "PHP 7.x is recommended for both security and language features.\\nWould you like to install PHP7 via Remi's RPM repository?\\n\\nSee: https://rpms.remirepo.net for more information" ${r} ${c}; then # User decided to NOT update PHP from REMI, attempt to install the default available PHP version echo -e " ${INFO} User opt-out of PHP 7 upgrade on CentOS. Deprecated PHP may be in use." : # continue with unsupported php version else echo -e " ${INFO} Enabling Remi's RPM repository (https://rpms.remirepo.net)" "${PKG_INSTALL[@]}" "https://rpms.remirepo.net/enterprise/${REMI_PKG}-$(rpm -E '%{rhel}').rpm" &> /dev/null # enable the PHP 7 repository via yum-config-manager (provided by yum-utils) "${PKG_INSTALL[@]}" "yum-utils" &> /dev/null yum-config-manager --enable ${REMI_REPO} &> /dev/null echo -e " ${TICK} Remi's RPM repository has been enabled for PHP7" fi fi fi else # If not a supported version of Fedora or CentOS, echo -e " ${CROSS} Unsupported RPM based distribution" # exit the installer exit fi # If neither apt-get or rmp/dnf are found else # it's not an OS we can support, echo -e " ${CROSS} OS distribution not supported" # so exit the installer exit fi } # A function for checking if a folder is a git repository is_repo() { # Use 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 local variable for the current directory local curdir # A variable to store the return code local rc # Assign the current directory variable by using pwd curdir="${PWD}" # If the first argument passed to this function is a directory, if [[ -d "${directory}" ]]; then # move into the directory cd "${directory}" # Use git to check if the folder is a repo # git -C is not used here to support git versions older than 1.8.4 git status --short &> /dev/null || rc=$? # If the command was not successful, else # Set a non-zero return code if directory does not exist rc=1 fi # Move back into the directory the user started in cd "${curdir}" # Return the code; if one is not set, return 0 return "${rc:-0}" } # A function to clone a repo make_repo() { # Set named variables for better readability local directory="${1}" local remoteRepo="${2}" # The message to display when this function is running str="Clone ${remoteRepo} into ${directory}" # Display the message and use the color table to preface the message with an "info" indicator echo -ne " ${INFO} ${str}..." # If the directory exists, if [[ -d "${directory}" ]]; then # delete everything in it so git can clone into it rm -rf "${directory}" fi # Clone the repo and return the return code from this command git clone -q --depth 1 "${remoteRepo}" "${directory}" &> /dev/null || return $? # Show a colored message showing it's status echo -e "${OVER} ${TICK} ${str}" # Always return 0? Not sure this is correct return 0 } # We need to make sure the repos are up-to-date so we can effectively install Clean out the directory if it exists for git to clone into update_repo() { # Use named, local variables # As you can see, these are the same variable names used in the last function, # but since they are local, their scope does not go beyond this function # This helps prevent the wrong value from being assigned if you were to set the variable as a GLOBAL one local directory="${1}" local curdir # A variable to store the message we want to display; # Again, it's useful to store these in variables in case we need to reuse or change the message; # we only need to make one change here local str="Update repo in ${1}" # Make sure we know what directory we are in so we can move back into it curdir="${PWD}" # Move into the directory that was passed as an argument cd "${directory}" &> /dev/null || return 1 # Let the user know what's happening echo -ne " ${INFO} ${str}..." # Stash any local commits as they conflict with our working code git stash --all --quiet &> /dev/null || true # Okay for stash failure git clean --quiet --force -d || true # Okay for already clean directory # Pull the latest commits git pull --quiet &> /dev/null || return $? # Show a completion message echo -e "${OVER} ${TICK} ${str}" # Move back into the original directory cd "${curdir}" &> /dev/null || return 1 return 0 } # A function that combines the functions previously made 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 echo -ne " ${INFO} ${str}..." # Check if the directory is a repository if is_repo "${directory}"; then # Show that we're checking it echo -e "${OVER} ${TICK} ${str}" # Update the repo, returning an error message on failure update_repo "${directory}" || { echo -e "\\n ${COL_LIGHT_RED}Error: Could not update local repository. Contact support.${COL_NC}"; exit 1; } # If it's not a .git repo, else # Show an error echo -e "${OVER} ${CROSS} ${str}" # Attempt to make the repository, showing an error on failure make_repo "${directory}" "${remoteRepo}" || { echo -e "\\n ${COL_LIGHT_RED}Error: Could not update local repository. Contact support.${COL_NC}"; exit 1; } fi # echo a blank line echo "" # and return success? return 0 } # Reset a repo to get rid of any local changed resetRepo() { # Use named variables for arguments local directory="${1}" # Move into the directory cd "${directory}" &> /dev/null || return 1 # Store the message in a variable str="Resetting repository within ${1}..." # Show the message echo -ne " ${INFO} ${str}" # Use git to remove the local changes git reset --hard &> /dev/null || return $? # And show the status echo -e "${OVER} ${TICK} ${str}" # Returning success anyway? return 0 } # We need to know the IPv4 information so we can effectively setup the DNS server # Without this information, we won't know where to Pi-hole will be found find_IPv4_information() { # Named, local variables local route # Find IP used to route to outside world by checking the the route to Google's public DNS server route=$(ip route get 8.8.8.8) # Use awk to strip out just the interface device as it is used in future commands IPv4dev=$(awk '{for (i=1; i<=NF; i++) if ($i~/dev/) print $(i+1)}' <<< "${route}") # Get just the IP address IPv4bare=$(awk '{print $7}' <<< "${route}") # Append the CIDR notation to the IP address IPV4_ADDRESS=$(ip -o -f inet addr show | grep "${IPv4bare}" | awk '{print $4}' | awk 'END {print}') # Get the default gateway (the way to reach the Internet) IPv4gw=$(awk '{print $3}' <<< "${route}") } # Get available interfaces that are UP get_available_interfaces() { # 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 whiptail --msgbox --backtitle "Welcome" --title "Pi-hole automated installer" "\\n\\nThis installer will transform your device into a network-wide ad blocker!" ${r} ${c} # Request that users donate if they enjoy the software since we all work on it in our free time whiptail --msgbox --backtitle "Plea" --title "Free and open source" "\\n\\nThe Pi-hole is free, but powered by your donations: http://pi-hole.net/donate" ${r} ${c} # Explain the need for a static address whiptail --msgbox --backtitle "Initiating network interface" --title "Static IP Needed" "\\n\\nThe Pi-hole is a SERVER so it needs a STATIC IP ADDRESS to function properly. In the next section, you can choose to use your current network settings (DHCP) or to manually edit them." ${r} ${c} } # We need to make sure there is enough space before installing, so there is a function to check this verifyFreeDiskSpace() { # 50MB is the minimum space needed (45MB install (includes web admin bootstrap/jquery libraries etc) + 5MB one day of logs.) # - Fourdee: Local ensures the variable is only created, and accessible within this function/void. Generally considered a "good" coding practice for non-global variables. local str="Disk space check" # Required space in KB local required_free_kilobytes=51200 # Calculate existing free space on this machine local existing_free_kilobytes existing_free_kilobytes=$(df -Pk | grep -m1 '\/$' | awk '{print $4}') # If the existing space is not an integer, if ! [[ "${existing_free_kilobytes}" =~ ^([0-9])+$ ]]; then # show an error that we can't determine the free space echo -e " ${CROSS} ${str}" echo -e " ${INFO} Unknown free disk space!" echo -e " ${INFO} We were unable to determine available free disk space on this system." echo -e " ${INFO} You may override this check, however, it is not recommended" echo -e " ${INFO} The option '${COL_LIGHT_RED}--i_do_not_follow_recommendations${COL_NC}' can override this" echo -e " ${INFO} e.g: curl -L https://install.pi-hole.net | bash /dev/stdin ${COL_LIGHT_RED}