#!/usr/bin/env bash # shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # # 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 -L 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 propogate 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 # We write to a temporary file before moving the log to the pihole folder tmpLog=/tmp/pihole-install.log instalLogLoc=/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 piholes 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 # 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=true # 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 # If the color table file exists, if [[ -f "${coltable}" ]]; then # source it source ${coltable} # Othwerise, 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 # We prefer the php metapackage if it's there if ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1; then phpVer="php" # If not, else # fall back on the php5 packages phpVer="php5" 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 successfuly 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 dnsmasq dnsutils iputils-ping lsof netcat sudo unzip wget idn2) # 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" # The DNS server user DNSMASQ_USER="dnsmasq" # 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) PIHOLE_DEPS=(bc bind-utils cronie curl dnsmasq findutils nmap-ncat sudo unzip wget idn2) PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php php-common php-cli php-pdo) if ! grep -q 'Fedora' /etc/redhat-release; then INSTALLER_DEPS=("${INSTALLER_DEPS[@]}" "epel-release"); fi LIGHTTPD_USER="lighttpd" LIGHTTPD_GROUP="lighttpd" LIGHTTPD_CFG="lighttpd.conf.fedora" DNSMASQ_USER="nobody" # 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 arguement 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 oiginal 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 varible 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 falure 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 varibles for arguments local directory="${1}" # Move into the directory cd "${directory}" &> /dev/null || return 1 # Store the message in a varible 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 approriately 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" # Reqired 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} Unknown free disk space! We were unable to determine available free disk space on this system. You may override this check, however, it is not recommended The option '${COL_LIGHT_RED}--i_do_not_follow_recommendations${COL_NC}' can override this e.g: curl -L https://install.pi-hole.net | bash /dev/stdin ${COL_LIGHT_RED}