diff --git a/README.md b/README.md index fb2179eb..97459442 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ While quite outdated at this point, [this original blog post about Pi-hole](http ----- ## Coverage -- [Lifehacker: Turn A Raspberry Pi Into An Ad Blocker With A Single Command](https://www.lifehacker.com.au/2015/02/turn-a-raspberry-pi-into-an-ad-blocker-with-a-single-command/) (Feburary, 2015) +- [Lifehacker: Turn A Raspberry Pi Into An Ad Blocker With A Single Command](https://www.lifehacker.com.au/2015/02/turn-a-raspberry-pi-into-an-ad-blocker-with-a-single-command/) (February, 2015) - [MakeUseOf: Adblock Everywhere: The Raspberry Pi-Hole Way](http://www.makeuseof.com/tag/adblock-everywhere-raspberry-pi-hole-way/) (March, 2015) - [Catchpoint: Ad-Blocking on Apple iOS9: Valuing the End User Experience](http://blog.catchpoint.com/2015/09/14/ad-blocking-apple/) (September, 2015) - [Security Now Netcast: Pi-hole](https://www.youtube.com/watch?v=p7-osq_y8i8&t=100m26s) (October, 2015) diff --git a/advanced/01-pihole.conf b/advanced/01-pihole.conf index 38d2c0b5..2c8b3749 100644 --- a/advanced/01-pihole.conf +++ b/advanced/01-pihole.conf @@ -18,9 +18,8 @@ # WITHIN /etc/dnsmasq.d/yourname.conf # ############################################################################### -addn-hosts=/etc/pihole/gravity.list -addn-hosts=/etc/pihole/black.list addn-hosts=/etc/pihole/local.list +addn-hosts=/etc/pihole/custom.list domain-needed diff --git a/advanced/Scripts/database_migration/gravity-db.sh b/advanced/Scripts/database_migration/gravity-db.sh new file mode 100644 index 00000000..8a669429 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity-db.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1090 + +# Pi-hole: A black hole for Internet advertisements +# (c) 2019 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# Updates gravity.db database +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +readonly scriptPath="/etc/.pihole/advanced/Scripts/database_migration/gravity" + +upgrade_gravityDB(){ + local database piholeDir auditFile version + database="${1}" + piholeDir="${2}" + auditFile="${piholeDir}/auditlog.list" + + # Get database version + version="$(sqlite3 "${database}" "SELECT \"value\" FROM \"info\" WHERE \"property\" = 'version';")" + + if [[ "$version" == "1" ]]; then + # This migration script upgrades the gravity.db file by + # adding the domain_audit table + echo -e " ${INFO} Upgrading gravity database from version 1 to 2" + sqlite3 "${database}" < "${scriptPath}/1_to_2.sql" + version=2 + + # Store audit domains in database table + if [ -e "${auditFile}" ]; then + echo -e " ${INFO} Migrating content of ${auditFile} into new database" + # database_table_from_file is defined in gravity.sh + database_table_from_file "domain_audit" "${auditFile}" + fi + fi + if [[ "$version" == "2" ]]; then + # This migration script upgrades the gravity.db file by + # renaming the regex table to regex_blacklist, and + # creating a new regex_whitelist table + corresponding linking table and views + echo -e " ${INFO} Upgrading gravity database from version 2 to 3" + sqlite3 "${database}" < "${scriptPath}/2_to_3.sql" + version=3 + fi + if [[ "$version" == "3" ]]; then + # This migration script unifies the formally separated domain + # lists into a single table with a UNIQUE domain constraint + echo -e " ${INFO} Upgrading gravity database from version 3 to 4" + sqlite3 "${database}" < "${scriptPath}/3_to_4.sql" + version=4 + fi + if [[ "$version" == "4" ]]; then + # This migration script upgrades the gravity and list views + # implementing necessary changes for per-client blocking + echo -e " ${INFO} Upgrading gravity database from version 4 to 5" + sqlite3 "${database}" < "${scriptPath}/4_to_5.sql" + version=5 + fi + if [[ "$version" == "5" ]]; then + # This migration script upgrades the adlist view + # to return an ID used in gravity.sh + echo -e " ${INFO} Upgrading gravity database from version 5 to 6" + sqlite3 "${database}" < "${scriptPath}/5_to_6.sql" + version=6 + fi + if [[ "$version" == "6" ]]; then + # This migration script adds a special group with ID 0 + # which is automatically associated to all clients not + # having their own group assignments + echo -e " ${INFO} Upgrading gravity database from version 6 to 7" + sqlite3 "${database}" < "${scriptPath}/6_to_7.sql" + version=7 + fi + if [[ "$version" == "7" ]]; then + # This migration script recreated the group table + # to ensure uniqueness on the group name + # We also add date_added and date_modified columns + echo -e " ${INFO} Upgrading gravity database from version 7 to 8" + sqlite3 "${database}" < "${scriptPath}/7_to_8.sql" + version=8 + fi + if [[ "$version" == "8" ]]; then + # This migration fixes some issues that were introduced + # in the previous migration script. + echo -e " ${INFO} Upgrading gravity database from version 8 to 9" + sqlite3 "${database}" < "${scriptPath}/8_to_9.sql" + version=9 + fi + if [[ "$version" == "9" ]]; then + # This migration drops unused tables and creates triggers to remove + # obsolete groups assignments when the linked items are deleted + echo -e " ${INFO} Upgrading gravity database from version 9 to 10" + sqlite3 "${database}" < "${scriptPath}/9_to_10.sql" + version=10 + fi + if [[ "$version" == "10" ]]; then + # This adds timestamp and an optional comment field to the client table + # These fields are only temporary and will be replaces by the columns + # defined in gravity.db.sql during gravity swapping. We add them here + # to keep the copying process generic (needs the same columns in both the + # source and the destination databases). + echo -e " ${INFO} Upgrading gravity database from version 10 to 11" + sqlite3 "${database}" < "${scriptPath}/10_to_11.sql" + version=11 + fi +} diff --git a/advanced/Scripts/database_migration/gravity/10_to_11.sql b/advanced/Scripts/database_migration/gravity/10_to_11.sql new file mode 100644 index 00000000..b073f83b --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/10_to_11.sql @@ -0,0 +1,16 @@ +.timeout 30000 + +BEGIN TRANSACTION; + +ALTER TABLE client ADD COLUMN date_added INTEGER; +ALTER TABLE client ADD COLUMN date_modified INTEGER; +ALTER TABLE client ADD COLUMN comment TEXT; + +CREATE TRIGGER tr_client_update AFTER UPDATE ON client + BEGIN + UPDATE client SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +UPDATE info SET value = 11 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/1_to_2.sql b/advanced/Scripts/database_migration/gravity/1_to_2.sql new file mode 100644 index 00000000..6d57a6fe --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/1_to_2.sql @@ -0,0 +1,14 @@ +.timeout 30000 + +BEGIN TRANSACTION; + +CREATE TABLE domain_audit +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT UNIQUE NOT NULL, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)) +); + +UPDATE info SET value = 2 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/2_to_3.sql b/advanced/Scripts/database_migration/gravity/2_to_3.sql new file mode 100644 index 00000000..fd7c24d2 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/2_to_3.sql @@ -0,0 +1,65 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +ALTER TABLE regex RENAME TO regex_blacklist; + +CREATE TABLE regex_blacklist_by_group +( + regex_blacklist_id INTEGER NOT NULL REFERENCES regex_blacklist (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (regex_blacklist_id, group_id) +); + +INSERT INTO regex_blacklist_by_group SELECT * FROM regex_by_group; +DROP TABLE regex_by_group; +DROP VIEW vw_regex; +DROP TRIGGER tr_regex_update; + +CREATE VIEW vw_regex_blacklist AS SELECT DISTINCT domain + FROM regex_blacklist + LEFT JOIN regex_blacklist_by_group ON regex_blacklist_by_group.regex_blacklist_id = regex_blacklist.id + LEFT JOIN "group" ON "group".id = regex_blacklist_by_group.group_id + WHERE regex_blacklist.enabled = 1 AND (regex_blacklist_by_group.group_id IS NULL OR "group".enabled = 1) + ORDER BY regex_blacklist.id; + +CREATE TRIGGER tr_regex_blacklist_update AFTER UPDATE ON regex_blacklist + BEGIN + UPDATE regex_blacklist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + END; + +CREATE TABLE regex_whitelist +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT UNIQUE NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 1, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + comment TEXT +); + +CREATE TABLE regex_whitelist_by_group +( + regex_whitelist_id INTEGER NOT NULL REFERENCES regex_whitelist (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (regex_whitelist_id, group_id) +); + +CREATE VIEW vw_regex_whitelist AS SELECT DISTINCT domain + FROM regex_whitelist + LEFT JOIN regex_whitelist_by_group ON regex_whitelist_by_group.regex_whitelist_id = regex_whitelist.id + LEFT JOIN "group" ON "group".id = regex_whitelist_by_group.group_id + WHERE regex_whitelist.enabled = 1 AND (regex_whitelist_by_group.group_id IS NULL OR "group".enabled = 1) + ORDER BY regex_whitelist.id; + +CREATE TRIGGER tr_regex_whitelist_update AFTER UPDATE ON regex_whitelist + BEGIN + UPDATE regex_whitelist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + END; + + +UPDATE info SET value = 3 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/3_to_4.sql b/advanced/Scripts/database_migration/gravity/3_to_4.sql new file mode 100644 index 00000000..352b1baa --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/3_to_4.sql @@ -0,0 +1,96 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE domainlist +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type INTEGER NOT NULL DEFAULT 0, + domain TEXT UNIQUE NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 1, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + comment TEXT +); + +ALTER TABLE whitelist ADD COLUMN type INTEGER; +UPDATE whitelist SET type = 0; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM whitelist; + +ALTER TABLE blacklist ADD COLUMN type INTEGER; +UPDATE blacklist SET type = 1; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM blacklist; + +ALTER TABLE regex_whitelist ADD COLUMN type INTEGER; +UPDATE regex_whitelist SET type = 2; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM regex_whitelist; + +ALTER TABLE regex_blacklist ADD COLUMN type INTEGER; +UPDATE regex_blacklist SET type = 3; +INSERT INTO domainlist (type,domain,enabled,date_added,date_modified,comment) + SELECT type,domain,enabled,date_added,date_modified,comment FROM regex_blacklist; + +DROP TABLE whitelist_by_group; +DROP TABLE blacklist_by_group; +DROP TABLE regex_whitelist_by_group; +DROP TABLE regex_blacklist_by_group; +CREATE TABLE domainlist_by_group +( + domainlist_id INTEGER NOT NULL REFERENCES domainlist (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (domainlist_id, group_id) +); + +DROP TRIGGER tr_whitelist_update; +DROP TRIGGER tr_blacklist_update; +DROP TRIGGER tr_regex_whitelist_update; +DROP TRIGGER tr_regex_blacklist_update; +CREATE TRIGGER tr_domainlist_update AFTER UPDATE ON domainlist + BEGIN + UPDATE domainlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + END; + +DROP VIEW vw_whitelist; +CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 0 + ORDER BY domainlist.id; + +DROP VIEW vw_blacklist; +CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 1 + ORDER BY domainlist.id; + +DROP VIEW vw_regex_whitelist; +CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 2 + ORDER BY domainlist.id; + +DROP VIEW vw_regex_blacklist; +CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 3 + ORDER BY domainlist.id; + +UPDATE info SET value = 4 WHERE property = 'version'; + +COMMIT; \ No newline at end of file diff --git a/advanced/Scripts/database_migration/gravity/4_to_5.sql b/advanced/Scripts/database_migration/gravity/4_to_5.sql new file mode 100644 index 00000000..2ad906fc --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/4_to_5.sql @@ -0,0 +1,38 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP TABLE gravity; +CREATE TABLE gravity +( + domain TEXT NOT NULL, + adlist_id INTEGER NOT NULL REFERENCES adlist (id), + PRIMARY KEY(domain, adlist_id) +); + +DROP VIEW vw_gravity; +CREATE VIEW vw_gravity AS SELECT domain, adlist_by_group.group_id AS group_id + FROM gravity + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = gravity.adlist_id + LEFT JOIN adlist ON adlist.id = gravity.adlist_id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1); + +CREATE TABLE client +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOL NULL UNIQUE +); + +CREATE TABLE client_by_group +( + client_id INTEGER NOT NULL REFERENCES client (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (client_id, group_id) +); + +UPDATE info SET value = 5 WHERE property = 'version'; + +COMMIT; \ No newline at end of file diff --git a/advanced/Scripts/database_migration/gravity/5_to_6.sql b/advanced/Scripts/database_migration/gravity/5_to_6.sql new file mode 100644 index 00000000..d2bb3145 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/5_to_6.sql @@ -0,0 +1,18 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP VIEW vw_adlist; +CREATE VIEW vw_adlist AS SELECT DISTINCT address, adlist.id AS id + FROM adlist + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = adlist.id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1) + ORDER BY adlist.id; + +UPDATE info SET value = 6 WHERE property = 'version'; + +COMMIT; + diff --git a/advanced/Scripts/database_migration/gravity/6_to_7.sql b/advanced/Scripts/database_migration/gravity/6_to_7.sql new file mode 100644 index 00000000..22d9dfaf --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/6_to_7.sql @@ -0,0 +1,35 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +INSERT OR REPLACE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + +INSERT INTO domainlist_by_group (domainlist_id, group_id) SELECT id, 0 FROM domainlist; +INSERT INTO client_by_group (client_id, group_id) SELECT id, 0 FROM client; +INSERT INTO adlist_by_group (adlist_id, group_id) SELECT id, 0 FROM adlist; + +CREATE TRIGGER tr_domainlist_add AFTER INSERT ON domainlist + BEGIN + INSERT INTO domainlist_by_group (domainlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_client_add AFTER INSERT ON client + BEGIN + INSERT INTO client_by_group (client_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_adlist_add AFTER INSERT ON adlist + BEGIN + INSERT INTO adlist_by_group (adlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR REPLACE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +UPDATE info SET value = 7 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/7_to_8.sql b/advanced/Scripts/database_migration/gravity/7_to_8.sql new file mode 100644 index 00000000..ccf0c148 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/7_to_8.sql @@ -0,0 +1,35 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +ALTER TABLE "group" RENAME TO "group__"; + +CREATE TABLE "group" +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled BOOLEAN NOT NULL DEFAULT 1, + name TEXT UNIQUE NOT NULL, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + description TEXT +); + +CREATE TRIGGER tr_group_update AFTER UPDATE ON "group" + BEGIN + UPDATE "group" SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +INSERT OR IGNORE INTO "group" (id,enabled,name,description) SELECT id,enabled,name,description FROM "group__"; + +DROP TABLE "group__"; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +UPDATE info SET value = 8 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/8_to_9.sql b/advanced/Scripts/database_migration/gravity/8_to_9.sql new file mode 100644 index 00000000..0d873e2a --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/8_to_9.sql @@ -0,0 +1,27 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP TRIGGER IF EXISTS tr_group_update; +DROP TRIGGER IF EXISTS tr_group_zero; + +PRAGMA legacy_alter_table=ON; +ALTER TABLE "group" RENAME TO "group__"; +PRAGMA legacy_alter_table=OFF; +ALTER TABLE "group__" RENAME TO "group"; + +CREATE TRIGGER tr_group_update AFTER UPDATE ON "group" + BEGIN + UPDATE "group" SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +UPDATE info SET value = 9 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/database_migration/gravity/9_to_10.sql b/advanced/Scripts/database_migration/gravity/9_to_10.sql new file mode 100644 index 00000000..a5636a23 --- /dev/null +++ b/advanced/Scripts/database_migration/gravity/9_to_10.sql @@ -0,0 +1,29 @@ +.timeout 30000 + +PRAGMA FOREIGN_KEYS=OFF; + +BEGIN TRANSACTION; + +DROP TABLE IF EXISTS whitelist; +DROP TABLE IF EXISTS blacklist; +DROP TABLE IF EXISTS regex_whitelist; +DROP TABLE IF EXISTS regex_blacklist; + +CREATE TRIGGER tr_domainlist_delete AFTER DELETE ON domainlist + BEGIN + DELETE FROM domainlist_by_group WHERE domainlist_id = OLD.id; + END; + +CREATE TRIGGER tr_adlist_delete AFTER DELETE ON adlist + BEGIN + DELETE FROM adlist_by_group WHERE adlist_id = OLD.id; + END; + +CREATE TRIGGER tr_client_delete AFTER DELETE ON client + BEGIN + DELETE FROM client_by_group WHERE client_id = OLD.id; + END; + +UPDATE info SET value = 10 WHERE property = 'version'; + +COMMIT; diff --git a/advanced/Scripts/list.sh b/advanced/Scripts/list.sh index c1d95aae..4f2e046f 100755 --- a/advanced/Scripts/list.sh +++ b/advanced/Scripts/list.sh @@ -11,69 +11,84 @@ # Globals basename=pihole piholeDir=/etc/"${basename}" -whitelist="${piholeDir}"/whitelist.txt -blacklist="${piholeDir}"/blacklist.txt +gravityDBfile="${piholeDir}/gravity.db" -readonly regexlist="/etc/pihole/regex.list" reload=false addmode=true verbose=true wildcard=false +web=false domList=() -listMain="" -listAlt="" +typeId="" colfile="/opt/pihole/COL_TABLE" source ${colfile} +# IDs are hard-wired to domain interpretation in the gravity database scheme +# Clients (including FTL) will read them through the corresponding views +readonly whitelist="0" +readonly blacklist="1" +readonly regex_whitelist="2" +readonly regex_blacklist="3" + +GetListnameFromTypeId() { + if [[ "$1" == "${whitelist}" ]]; then + echo "whitelist" + elif [[ "$1" == "${blacklist}" ]]; then + echo "blacklist" + elif [[ "$1" == "${regex_whitelist}" ]]; then + echo "regex whitelist" + elif [[ "$1" == "${regex_blacklist}" ]]; then + echo "regex blacklist" + fi +} + +GetListParamFromTypeId() { + if [[ "${typeId}" == "${whitelist}" ]]; then + echo "w" + elif [[ "${typeId}" == "${blacklist}" ]]; then + echo "b" + elif [[ "${typeId}" == "${regex_whitelist}" && "${wildcard}" == true ]]; then + echo "-white-wild" + elif [[ "${typeId}" == "${regex_whitelist}" ]]; then + echo "-white-regex" + elif [[ "${typeId}" == "${regex_blacklist}" && "${wildcard}" == true ]]; then + echo "-wild" + elif [[ "${typeId}" == "${regex_blacklist}" ]]; then + echo "-regex" + fi +} helpFunc() { - if [[ "${listMain}" == "${whitelist}" ]]; then - param="w" - type="white" - elif [[ "${listMain}" == "${regexlist}" && "${wildcard}" == true ]]; then - param="-wild" - type="wildcard black" - elif [[ "${listMain}" == "${regexlist}" ]]; then - param="-regex" - type="regex black" - else - param="b" - type="black" - fi + local listname param + + listname="$(GetListnameFromTypeId "${typeId}")" + param="$(GetListParamFromTypeId)" echo "Usage: pihole -${param} [options] Example: 'pihole -${param} site.com', or 'pihole -${param} site1.com site2.com' -${type^}list one or more domains +${listname^} one or more domains Options: - -d, --delmode Remove domain(s) from the ${type}list - -nr, --noreload Update ${type}list without refreshing dnsmasq + -d, --delmode Remove domain(s) from the ${listname} + -nr, --noreload Update ${listname} without reloading the DNS server -q, --quiet Make output less verbose -h, --help Show this help dialog - -l, --list Display all your ${type}listed domains + -l, --list Display all your ${listname}listed domains --nuke Removes all entries in a list" exit 0 } -EscapeRegexp() { - # This way we may safely insert an arbitrary - # string in our regular expressions - # This sed is intentionally executed in three steps to ease maintainability - # The first sed removes any amount of leading dots - echo $* | sed 's/^\.*//' | sed "s/[]\.|$(){}?+*^]/\\\\&/g" | sed "s/\\//\\\\\//g" -} - -HandleOther() { +ValidateDomain() { # Convert to lowercase domain="${1,,}" # Check validity of domain (don't check for regex entries) if [[ "${#domain}" -le 253 ]]; then - if [[ "${listMain}" == "${regexlist}" && "${wildcard}" == false ]]; then + if [[ ( "${typeId}" == "${regex_blacklist}" || "${typeId}" == "${regex_whitelist}" ) && "${wildcard}" == false ]]; then validDomain="${domain}" else validDomain=$(grep -P "^((-|_)*[a-z\\d]((-|_)*[a-z\\d])*(-|_)*)(\\.(-|_)*([a-z\\d]((-|_)*[a-z\\d])*))*$" <<< "${domain}") # Valid chars check @@ -88,176 +103,143 @@ HandleOther() { fi } -PoplistFile() { - # Check whitelist file exists, and if not, create it - if [[ ! -f "${whitelist}" ]]; then - touch "${whitelist}" - fi - - # Check blacklist file exists, and if not, create it - if [[ ! -f "${blacklist}" ]]; then - touch "${blacklist}" - fi - +ProcessDomainList() { for dom in "${domList[@]}"; do - # Logic: If addmode then add to desired list and remove from the other; if delmode then remove from desired list but do not add to the other + # Format domain into regex filter if requested + if [[ "${wildcard}" == true ]]; then + dom="(^|\\.)${dom//\./\\.}$" + fi + + # Logic: If addmode then add to desired list and remove from the other; + # if delmode then remove from desired list but do not add to the other if ${addmode}; then - AddDomain "${dom}" "${listMain}" - RemoveDomain "${dom}" "${listAlt}" + AddDomain "${dom}" else - RemoveDomain "${dom}" "${listMain}" + RemoveDomain "${dom}" fi done } AddDomain() { - list="$2" - domain=$(EscapeRegexp "$1") + local domain num requestedListname existingTypeId existingListname + domain="$1" - [[ "${list}" == "${whitelist}" ]] && listname="whitelist" - [[ "${list}" == "${blacklist}" ]] && listname="blacklist" + # Is the domain in the list we want to add it to? + num="$(sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}';")" + requestedListname="$(GetListnameFromTypeId "${typeId}")" - if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then - [[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only" - [[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only" - bool=true - # Is the domain in the list we want to add it to? - grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false - - if [[ "${bool}" == false ]]; then - # Domain not found in the whitelist file, add it! - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} Adding ${1} to ${listname}..." - fi - reload=true - # Add it to the list we want to add it to - echo "$1" >> "${list}" - else - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} ${1} already exists in ${listname}, no need to add!" - fi + if [[ "${num}" -ne 0 ]]; then + existingTypeId="$(sqlite3 "${gravityDBfile}" "SELECT type FROM domainlist WHERE domain = '${domain}';")" + if [[ "${existingTypeId}" == "${typeId}" ]]; then + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${1} already exists in ${requestedListname}, no need to add!" fi - elif [[ "${list}" == "${regexlist}" ]]; then - [[ -z "${type}" ]] && type="--wildcard-only" - bool=true - domain="${1}" - - [[ "${wildcard}" == true ]] && domain="(^|\\.)${domain//\./\\.}$" - - # Is the domain in the list? - # Search only for exactly matching lines - grep -Fx "${domain}" "${regexlist}" > /dev/null 2>&1 || bool=false - - if [[ "${bool}" == false ]]; then - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} Adding ${domain} to regex list..." - fi - reload="restart" - echo "$domain" >> "${regexlist}" - else - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} ${domain} already exists in regex list, no need to add!" - fi + else + existingListname="$(GetListnameFromTypeId "${existingTypeId}")" + sqlite3 "${gravityDBfile}" "UPDATE domainlist SET type = ${typeId} WHERE domain='${domain}';" + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${1} already exists in ${existingListname}, it has been moved to ${requestedListname}!" fi + fi + return fi + + # Domain not found in the table, add it! + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} Adding ${domain} to the ${requestedListname}..." + fi + reload=true + # Insert only the domain here. The enabled and date_added fields will be filled + # with their default values (enabled = true, date_added = current timestamp) + sqlite3 "${gravityDBfile}" "INSERT INTO domainlist (domain,type) VALUES ('${domain}',${typeId});" } RemoveDomain() { - list="$2" - domain=$(EscapeRegexp "$1") + local domain num requestedListname + domain="$1" - [[ "${list}" == "${whitelist}" ]] && listname="whitelist" - [[ "${list}" == "${blacklist}" ]] && listname="blacklist" + # Is the domain in the list we want to remove it from? + num="$(sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};")" - if [[ "${list}" == "${whitelist}" || "${list}" == "${blacklist}" ]]; then - bool=true - [[ "${list}" == "${whitelist}" && -z "${type}" ]] && type="--whitelist-only" - [[ "${list}" == "${blacklist}" && -z "${type}" ]] && type="--blacklist-only" - # Is it in the list? Logic follows that if its whitelisted it should not be blacklisted and vice versa - grep -Ex -q "${domain}" "${list}" > /dev/null 2>&1 || bool=false - if [[ "${bool}" == true ]]; then - # Remove it from the other one - echo -e " ${INFO} Removing $1 from ${listname}..." - # /I flag: search case-insensitive - sed -i "/${domain}/Id" "${list}" - reload=true - else - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} ${1} does not exist in ${listname}, no need to remove!" - fi - fi - elif [[ "${list}" == "${regexlist}" ]]; then - [[ -z "${type}" ]] && type="--wildcard-only" - domain="${1}" + requestedListname="$(GetListnameFromTypeId "${typeId}")" - [[ "${wildcard}" == true ]] && domain="(^|\\.)${domain//\./\\.}$" - - bool=true - # Is it in the list? - grep -Fx "${domain}" "${regexlist}" > /dev/null 2>&1 || bool=false - if [[ "${bool}" == true ]]; then - # Remove it from the other one - echo -e " ${INFO} Removing $domain from regex list..." - local lineNumber - lineNumber=$(grep -Fnx "$domain" "${list}" | cut -f1 -d:) - sed -i "${lineNumber}d" "${list}" - reload=true - else - if [[ "${verbose}" == true ]]; then - echo -e " ${INFO} ${domain} does not exist in regex list, no need to remove!" - fi - fi + if [[ "${num}" -eq 0 ]]; then + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} ${domain} does not exist in ${requestedListname}, no need to remove!" + fi + return fi -} -# Update Gravity -Reload() { - echo "" - pihole -g --skip-download "${type:-}" + # Domain found in the table, remove it! + if [[ "${verbose}" == true ]]; then + echo -e " ${INFO} Removing ${domain} from the ${requestedListname}..." + fi + reload=true + # Remove it from the current list + sqlite3 "${gravityDBfile}" "DELETE FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};" } Displaylist() { - if [[ -f ${listMain} ]]; then - if [[ "${listMain}" == "${whitelist}" ]]; then - string="gravity resistant domains" - else - string="domains caught in the sinkhole" - fi - verbose=false - echo -e "Displaying $string:\n" - count=1 - while IFS= read -r RD || [ -n "${RD}" ]; do - echo " ${count}: ${RD}" - count=$((count+1)) - done < "${listMain}" + local count num_pipes domain enabled status nicedate requestedListname + + requestedListname="$(GetListnameFromTypeId "${typeId}")" + data="$(sqlite3 "${gravityDBfile}" "SELECT domain,enabled,date_modified FROM domainlist WHERE type = ${typeId};" 2> /dev/null)" + + if [[ -z $data ]]; then + echo -e "Not showing empty list" else - echo -e " ${COL_LIGHT_RED}${listMain} does not exist!${COL_NC}" + echo -e "Displaying ${requestedListname}:" + count=1 + while IFS= read -r line + do + # Count number of pipes seen in this line + # This is necessary because we can only detect the pipe separating the fields + # from the end backwards as the domain (which is the first field) may contain + # pipe symbols as they are perfectly valid regex filter control characters + num_pipes="$(grep -c "^" <<< "$(grep -o "|" <<< "${line}")")" + + # Extract domain and enabled status based on the obtained number of pipe characters + domain="$(cut -d'|' -f"-$((num_pipes-1))" <<< "${line}")" + enabled="$(cut -d'|' -f"$((num_pipes))" <<< "${line}")" + datemod="$(cut -d'|' -f"$((num_pipes+1))" <<< "${line}")" + + # Translate boolean status into human readable string + if [[ "${enabled}" -eq 1 ]]; then + status="enabled" + else + status="disabled" + fi + + # Get nice representation of numerical date stored in database + nicedate=$(date --rfc-2822 -d "@${datemod}") + + echo " ${count}: ${domain} (${status}, last modified ${nicedate})" + count=$((count+1)) + done <<< "${data}" fi exit 0; } NukeList() { - if [[ -f "${listMain}" ]]; then - # Back up original list - cp "${listMain}" "${listMain}.bck~" - # Empty out file - echo "" > "${listMain}" - fi + sqlite3 "${gravityDBfile}" "DELETE FROM domainlist WHERE type = ${typeId};" } for var in "$@"; do case "${var}" in - "-w" | "whitelist" ) listMain="${whitelist}"; listAlt="${blacklist}";; - "-b" | "blacklist" ) listMain="${blacklist}"; listAlt="${whitelist}";; - "--wild" | "wildcard" ) listMain="${regexlist}"; wildcard=true;; - "--regex" | "regex" ) listMain="${regexlist}";; + "-w" | "whitelist" ) typeId=0;; + "-b" | "blacklist" ) typeId=1;; + "--white-regex" | "white-regex" ) typeId=2;; + "--white-wild" | "white-wild" ) typeId=2; wildcard=true;; + "--wild" | "wildcard" ) typeId=3; wildcard=true;; + "--regex" | "regex" ) typeId=3;; "-nr"| "--noreload" ) reload=false;; "-d" | "--delmode" ) addmode=false;; "-q" | "--quiet" ) verbose=false;; "-h" | "--help" ) helpFunc;; "-l" | "--list" ) Displaylist;; "--nuke" ) NukeList;; - * ) HandleOther "${var}";; + "--web" ) web=true;; + * ) ValidateDomain "${var}";; esac done @@ -267,9 +249,13 @@ if [[ $# = 0 ]]; then helpFunc fi -PoplistFile +ProcessDomainList + +# Used on web interface +if $web; then +echo "DONE" +fi if [[ "${reload}" != false ]]; then - # Ensure that "restart" is used for Wildcard updates - Reload "${reload}" + pihole restartdns reload-lists fi diff --git a/advanced/Scripts/piholeARPTable.sh b/advanced/Scripts/piholeARPTable.sh new file mode 100755 index 00000000..aa45f9ad --- /dev/null +++ b/advanced/Scripts/piholeARPTable.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1090 + +# Pi-hole: A black hole for Internet advertisements +# (c) 2019 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# ARP table interaction +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +coltable="/opt/pihole/COL_TABLE" +if [[ -f ${coltable} ]]; then + source ${coltable} +fi + +# Determine database location +# Obtain DBFILE=... setting from pihole-FTL.db +# Constructed to return nothing when +# a) the setting is not present in the config file, or +# b) the setting is commented out (e.g. "#DBFILE=...") +FTLconf="/etc/pihole/pihole-FTL.conf" +if [ -e "$FTLconf" ]; then + DBFILE="$(sed -n -e 's/^\s*DBFILE\s*=\s*//p' ${FTLconf})" +fi +# Test for empty string. Use standard path in this case. +if [ -z "$DBFILE" ]; then + DBFILE="/etc/pihole/pihole-FTL.db" +fi + + +flushARP(){ + local output + if [[ "${args[1]}" != "quiet" ]]; then + echo -ne " ${INFO} Flushing network table ..." + fi + + # Flush ARP cache to avoid re-adding of dead entries + if ! output=$(ip neigh flush all 2>&1); then + echo -e "${OVER} ${CROSS} Failed to clear ARP cache" + echo " Output: ${output}" + return 1 + fi + + # Truncate network_addresses table in pihole-FTL.db + # This needs to be done before we can truncate the network table due to + # foreign key contraints + if ! output=$(sqlite3 "${DBFILE}" "DELETE FROM network_addresses" 2>&1); then + echo -e "${OVER} ${CROSS} Failed to truncate network_addresses table" + echo " Database location: ${DBFILE}" + echo " Output: ${output}" + return 1 + fi + + # Truncate network table in pihole-FTL.db + if ! output=$(sqlite3 "${DBFILE}" "DELETE FROM network" 2>&1); then + echo -e "${OVER} ${CROSS} Failed to truncate network table" + echo " Database location: ${DBFILE}" + echo " Output: ${output}" + return 1 + fi + + if [[ "${args[1]}" != "quiet" ]]; then + echo -e "${OVER} ${TICK} Flushed network table" + fi +} + +args=("$@") + +case "${args[0]}" in + "arpflush" ) flushARP;; +esac diff --git a/advanced/Scripts/piholeCheckout.sh b/advanced/Scripts/piholeCheckout.sh index dd57117f..31009dd9 100644 --- a/advanced/Scripts/piholeCheckout.sh +++ b/advanced/Scripts/piholeCheckout.sh @@ -95,6 +95,7 @@ checkout() { local path path="development/${binary}" echo "development" > /etc/pihole/ftlbranch + chmod 644 /etc/pihole/ftlbranch elif [[ "${1}" == "master" ]] ; then # Shortcut to check out master branches echo -e " ${INFO} Shortcut \"master\" detected - checking out master branches..." @@ -108,6 +109,7 @@ checkout() { local path path="master/${binary}" echo "master" > /etc/pihole/ftlbranch + chmod 644 /etc/pihole/ftlbranch elif [[ "${1}" == "core" ]] ; then str="Fetching branches from ${piholeGitUrl}" echo -ne " ${INFO} $str" @@ -169,6 +171,7 @@ checkout() { if check_download_exists "$path"; then echo " ${TICK} Branch ${2} exists" echo "${2}" > /etc/pihole/ftlbranch + chmod 644 /etc/pihole/ftlbranch FTLinstall "${binary}" restart_service pihole-FTL enable_service pihole-FTL diff --git a/advanced/Scripts/piholeDebug.sh b/advanced/Scripts/piholeDebug.sh index d46944d6..28d34ab6 100755 --- a/advanced/Scripts/piholeDebug.sh +++ b/advanced/Scripts/piholeDebug.sh @@ -89,16 +89,40 @@ PIHOLE_WILDCARD_CONFIG_FILE="${DNSMASQ_D_DIRECTORY}/03-wildcard.conf" WEB_SERVER_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/lighttpd.conf" #WEB_SERVER_CUSTOM_CONFIG_FILE="${WEB_SERVER_CONFIG_DIRECTORY}/external.conf" -PIHOLE_DEFAULT_AD_LISTS="${PIHOLE_DIRECTORY}/adlists.default" -PIHOLE_USER_DEFINED_AD_LISTS="${PIHOLE_DIRECTORY}/adlists.list" -PIHOLE_BLACKLIST_FILE="${PIHOLE_DIRECTORY}/blacklist.txt" -PIHOLE_BLOCKLIST_FILE="${PIHOLE_DIRECTORY}/gravity.list" PIHOLE_INSTALL_LOG_FILE="${PIHOLE_DIRECTORY}/install.log" PIHOLE_RAW_BLOCKLIST_FILES="${PIHOLE_DIRECTORY}/list.*" PIHOLE_LOCAL_HOSTS_FILE="${PIHOLE_DIRECTORY}/local.list" PIHOLE_LOGROTATE_FILE="${PIHOLE_DIRECTORY}/logrotate" PIHOLE_SETUP_VARS_FILE="${PIHOLE_DIRECTORY}/setupVars.conf" -PIHOLE_WHITELIST_FILE="${PIHOLE_DIRECTORY}/whitelist.txt" +PIHOLE_FTL_CONF_FILE="${PIHOLE_DIRECTORY}/pihole-FTL.conf" + +# Read the value of an FTL config key. The value is printed to stdout. +# +# Args: +# 1. The key to read +# 2. The default if the setting or config does not exist +get_ftl_conf_value() { + local key=$1 + local default=$2 + local value + + # Obtain key=... setting from pihole-FTL.conf + if [[ -e "$PIHOLE_FTL_CONF_FILE" ]]; then + # Constructed to return nothing when + # a) the setting is not present in the config file, or + # b) the setting is commented out (e.g. "#DBFILE=...") + value="$(sed -n -e "s/^\\s*$key=\\s*//p" ${PIHOLE_FTL_CONF_FILE})" + fi + + # Test for missing value. Use default value in this case. + if [[ -z "$value" ]]; then + value="$default" + fi + + echo "$value" +} + +PIHOLE_GRAVITY_DB_FILE="$(get_ftl_conf_value "GRAVITYDB" "${PIHOLE_DIRECTORY}/gravity.db")" PIHOLE_COMMAND="${BIN_DIRECTORY}/pihole" PIHOLE_COLTABLE_FILE="${BIN_DIRECTORY}/COL_TABLE" @@ -109,7 +133,7 @@ FTL_PORT="${RUN_DIRECTORY}/pihole-FTL.port" PIHOLE_LOG="${LOG_DIRECTORY}/pihole.log" PIHOLE_LOG_GZIPS="${LOG_DIRECTORY}/pihole.log.[0-9].*" PIHOLE_DEBUG_LOG="${LOG_DIRECTORY}/pihole_debug.log" -PIHOLE_FTL_LOG="${LOG_DIRECTORY}/pihole-FTL.log" +PIHOLE_FTL_LOG="$(get_ftl_conf_value "LOGFILE" "${LOG_DIRECTORY}/pihole-FTL.log")" PIHOLE_WEB_SERVER_ACCESS_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/access.log" PIHOLE_WEB_SERVER_ERROR_LOG_FILE="${WEB_SERVER_LOG_DIRECTORY}/error.log" @@ -142,16 +166,11 @@ REQUIRED_FILES=("${PIHOLE_CRON_FILE}" "${PIHOLE_DHCP_CONFIG_FILE}" "${PIHOLE_WILDCARD_CONFIG_FILE}" "${WEB_SERVER_CONFIG_FILE}" -"${PIHOLE_DEFAULT_AD_LISTS}" -"${PIHOLE_USER_DEFINED_AD_LISTS}" -"${PIHOLE_BLACKLIST_FILE}" -"${PIHOLE_BLOCKLIST_FILE}" "${PIHOLE_INSTALL_LOG_FILE}" "${PIHOLE_RAW_BLOCKLIST_FILES}" "${PIHOLE_LOCAL_HOSTS_FILE}" "${PIHOLE_LOGROTATE_FILE}" "${PIHOLE_SETUP_VARS_FILE}" -"${PIHOLE_WHITELIST_FILE}" "${PIHOLE_COMMAND}" "${PIHOLE_COLTABLE_FILE}" "${FTL_PID}" @@ -795,7 +814,7 @@ dig_at() { # This helps emulate queries to different domains that a user might query # It will also give extra assurance that Pi-hole is correctly resolving and blocking domains local random_url - random_url=$(shuf -n 1 "${PIHOLE_BLOCKLIST_FILE}") + random_url=$(sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity ORDER BY RANDOM() LIMIT 1") # First, do a dig on localhost to see if Pi-hole can use itself to block a domain if local_dig=$(dig +tries=1 +time=2 -"${protocol}" "${random_url}" @${local_address} +short "${record_type}"); then @@ -977,8 +996,7 @@ list_files_in_dir() { if [[ -d "${dir_to_parse}/${each_file}" ]]; then # If it's a directoy, do nothing : - elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_BLOCKLIST_FILE}" ]] || \ - [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \ + elif [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_DEBUG_LOG}" ]] || \ [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_RAW_BLOCKLIST_FILES}" ]] || \ [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_INSTALL_LOG_FILE}" ]] || \ [[ "${dir_to_parse}/${each_file}" == "${PIHOLE_SETUP_VARS_FILE}" ]] || \ @@ -1063,31 +1081,74 @@ head_tail_log() { IFS="$OLD_IFS" } -analyze_gravity_list() { - echo_current_diagnostic "Gravity list" - local head_line - local tail_line - # Put the current Internal Field Separator into another variable so it can be restored later +show_db_entries() { + local title="${1}" + local query="${2}" + local widths="${3}" + + echo_current_diagnostic "${title}" + OLD_IFS="$IFS" - # Get the lines that are in the file(s) and store them in an array for parsing later IFS=$'\r\n' + local entries=() + mapfile -t entries < <(\ + sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" \ + -cmd ".headers on" \ + -cmd ".mode column" \ + -cmd ".width ${widths}" \ + "${query}"\ + ) + + for line in "${entries[@]}"; do + log_write " ${line}" + done + + IFS="$OLD_IFS" +} + +show_groups() { + show_db_entries "Groups" "SELECT id,name,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,description FROM \"group\"" "4 50 7 19 19 50" +} + +show_adlists() { + show_db_entries "Adlists" "SELECT id,address,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM adlist" "4 100 7 19 19 50" + show_db_entries "Adlist groups" "SELECT * FROM adlist_by_group" "4 4" +} + +show_domainlist() { + show_db_entries "Domainlist (0/1 = exact/regex whitelist, 2/3 = exact/regex blacklist)" "SELECT id,type,domain,enabled,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM domainlist" "4 4 100 7 19 19 50" + show_db_entries "Domainlist groups" "SELECT * FROM domainlist_by_group" "10 10" +} + +show_clients() { + show_db_entries "Clients" "SELECT id,ip,datetime(date_added,'unixepoch','localtime') date_added,datetime(date_modified,'unixepoch','localtime') date_modified,comment FROM client" "4 100 19 19 50" + show_db_entries "Client groups" "SELECT * FROM client_by_group" "10 10" +} + +analyze_gravity_list() { + echo_current_diagnostic "Gravity List and Database" + local gravity_permissions - gravity_permissions=$(ls -ld "${PIHOLE_BLOCKLIST_FILE}") + gravity_permissions=$(ls -ld "${PIHOLE_GRAVITY_DB_FILE}") log_write "${COL_GREEN}${gravity_permissions}${COL_NC}" - local gravity_head=() - mapfile -t gravity_head < <(head -n 4 ${PIHOLE_BLOCKLIST_FILE}) - log_write " ${COL_CYAN}-----head of $(basename ${PIHOLE_BLOCKLIST_FILE})------${COL_NC}" - for head_line in "${gravity_head[@]}"; do - log_write " ${head_line}" - done + + show_db_entries "Info table" "SELECT property,value FROM info" "20 40" + gravity_updated_raw="$(sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT value FROM info where property = 'updated'")" + gravity_updated="$(date -d @"${gravity_updated_raw}")" + log_write " Last gravity run finished at: ${COL_CYAN}${gravity_updated}${COL_NC}" log_write "" - local gravity_tail=() - mapfile -t gravity_tail < <(tail -n 4 ${PIHOLE_BLOCKLIST_FILE}) - log_write " ${COL_CYAN}-----tail of $(basename ${PIHOLE_BLOCKLIST_FILE})------${COL_NC}" - for tail_line in "${gravity_tail[@]}"; do - log_write " ${tail_line}" + + OLD_IFS="$IFS" + IFS=$'\r\n' + local gravity_sample=() + mapfile -t gravity_sample < <(sqlite3 "${PIHOLE_GRAVITY_DB_FILE}" "SELECT domain FROM vw_gravity LIMIT 10") + log_write " ${COL_CYAN}----- First 10 Gravity Domains -----${COL_NC}" + + for line in "${gravity_sample[@]}"; do + log_write " ${line}" done - # Set the IFS back to what it was + + log_write "" IFS="$OLD_IFS" } @@ -1238,6 +1299,10 @@ process_status parse_setup_vars check_x_headers analyze_gravity_list +show_groups +show_domainlist +show_clients +show_adlists show_content_of_pihole_files parse_locale analyze_pihole_log diff --git a/advanced/Scripts/piholeLogFlush.sh b/advanced/Scripts/piholeLogFlush.sh index 561fbce7..51e94d7c 100755 --- a/advanced/Scripts/piholeLogFlush.sh +++ b/advanced/Scripts/piholeLogFlush.sh @@ -39,8 +39,9 @@ if [[ "$@" == *"once"* ]]; then # Note that moving the file is not an option, as # dnsmasq would happily continue writing into the # moved file (it will have the same file handler) - cp /var/log/pihole.log /var/log/pihole.log.1 + cp -p /var/log/pihole.log /var/log/pihole.log.1 echo " " > /var/log/pihole.log + chmod 644 /var/log/pihole.log fi else # Manual flushing @@ -53,6 +54,7 @@ else echo " " > /var/log/pihole.log if [ -f /var/log/pihole.log.1 ]; then echo " " > /var/log/pihole.log.1 + chmod 644 /var/log/pihole.log.1 fi fi # Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history) diff --git a/advanced/Scripts/query.sh b/advanced/Scripts/query.sh old mode 100644 new mode 100755 index 2dead97d..a96129e0 --- a/advanced/Scripts/query.sh +++ b/advanced/Scripts/query.sh @@ -11,10 +11,8 @@ # Globals piholeDir="/etc/pihole" -adListsList="$piholeDir/adlists.list" -wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf" +gravityDBfile="${piholeDir}/gravity.db" options="$*" -adlist="" all="" exact="" blockpage="" @@ -23,27 +21,10 @@ matchType="match" colfile="/opt/pihole/COL_TABLE" source "${colfile}" -# Print each subdomain -# e.g: foo.bar.baz.com = "foo.bar.baz.com bar.baz.com baz.com com" -processWildcards() { - IFS="." read -r -a array <<< "${1}" - for (( i=${#array[@]}-1; i>=0; i-- )); do - ar="" - for (( j=${#array[@]}-1; j>${#array[@]}-i-2; j-- )); do - if [[ $j == $((${#array[@]}-1)) ]]; then - ar="${array[$j]}" - else - ar="${array[$j]}.${ar}" - fi - done - echo "${ar}" - done -} - # Scan an array of files for matching strings scanList(){ # Escape full stops - local domain="${1//./\\.}" lists="${2}" type="${3:-}" + local domain="${1}" esc_domain="${1//./\\.}" lists="${2}" type="${3:-}" # Prevent grep from printing file path cd "$piholeDir" || exit 1 @@ -54,9 +35,14 @@ scanList(){ # /dev/null forces filename to be printed when only one list has been generated # shellcheck disable=SC2086 case "${type}" in - "exact" ) grep -i -E "(^|\\s)${domain}($|\\s|#)" ${lists} /dev/null 2>/dev/null;; - "wc" ) grep -i -o -m 1 "/${domain}/" ${lists} 2>/dev/null;; - * ) grep -i "${domain}" ${lists} /dev/null 2>/dev/null;; + "exact" ) grep -i -E -l "(^|(?/dev/null;; + # Create array of regexps + # Iterate through each regexp and check whether it matches the domainQuery + # If it does, print the matching regexp and continue looping + # Input 1 - regexps | Input 2 - domainQuery + "regex" ) awk 'NR==FNR{regexps[$0];next}{for (r in regexps)if($0 ~ r)print r}' \ + <(echo "${lists}") <(echo "${domain}") 2>/dev/null;; + * ) grep -i "${esc_domain}" ${lists} /dev/null 2>/dev/null;; esac } @@ -66,23 +52,16 @@ Example: 'pihole -q -exact domain.com' Query the adlists for a specified domain Options: - -adlist Print the name of the block list URL -exact Search the block lists for exact domain matches -all Return all query matches within a block list -h, --help Show this help dialog" exit 0 fi -if [[ ! -e "$adListsList" ]]; then - echo -e "${COL_LIGHT_RED}The file $adListsList was not found${COL_NC}" - exit 1 -fi - # Handle valid options if [[ "${options}" == *"-bp"* ]]; then exact="exact"; blockpage=true else - [[ "${options}" == *"-adlist"* ]] && adlist=true [[ "${options}" == *"-all"* ]] && all=true if [[ "${options}" == *"-exact"* ]]; then exact="exact"; matchType="exact ${matchType}" @@ -107,69 +86,115 @@ if [[ -n "${str:-}" ]]; then exit 1 fi -# Scan Whitelist and Blacklist -lists="whitelist.txt blacklist.txt" -mapfile -t results <<< "$(scanList "${domainQuery}" "${lists}" "${exact}")" -if [[ -n "${results[*]}" ]]; then +scanDatabaseTable() { + local domain table type querystr result extra + domain="$(printf "%q" "${1}")" + table="${2}" + type="${3:-}" + + # As underscores are legitimate parts of domains, we escape them when using the LIKE operator. + # Underscores are SQLite wildcards matching exactly one character. We obviously want to suppress this + # behavior. The "ESCAPE '\'" clause specifies that an underscore preceded by an '\' should be matched + # as a literal underscore character. We pretreat the $domain variable accordingly to escape underscores. + if [[ "${table}" == "gravity" ]]; then + case "${exact}" in + "exact" ) querystr="SELECT gravity.domain,adlist.address,adlist.enabled FROM gravity LEFT JOIN adlist ON adlist.id = gravity.adlist_id WHERE domain = '${domain}'";; + * ) querystr="SELECT gravity.domain,adlist.address,adlist.enabled FROM gravity LEFT JOIN adlist ON adlist.id = gravity.adlist_id WHERE domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; + esac + else + case "${exact}" in + "exact" ) querystr="SELECT domain,enabled FROM domainlist WHERE type = '${type}' AND domain = '${domain}'";; + * ) querystr="SELECT domain,enabled FROM domainlist WHERE type = '${type}' AND domain LIKE '%${domain//_/\\_}%' ESCAPE '\\'";; + esac + fi + + # Send prepared query to gravity database + result="$(sqlite3 "${gravityDBfile}" "${querystr}")" 2> /dev/null + if [[ -z "${result}" ]]; then + # Return early when there are no matches in this table + return + fi + + if [[ "${table}" == "gravity" ]]; then + echo "${result}" + return + fi + + # Mark domain as having been white-/blacklist matched (global variable) wbMatch=true - # Loop through each result in order to print unique file title once + + # Print table name + if [[ -z "${blockpage}" ]]; then + echo " ${matchType^} found in ${COL_BOLD}exact ${table}${COL_NC}" + fi + + # Loop over results and print them + mapfile -t results <<< "${result}" for result in "${results[@]}"; do - fileName="${result%%.*}" if [[ -n "${blockpage}" ]]; then echo "π ${result}" exit 0 - elif [[ -n "${exact}" ]]; then - echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}" + fi + domain="${result/|*}" + if [[ "${result#*|}" == "0" ]]; then + extra=" (disabled)" else - # Only print filename title once per file - if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then - echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}" - fileName_prev="${fileName}" - fi - echo " ${result#*:}" + extra="" fi + echo " ${domain}${extra}" done -fi +} -# Scan Wildcards -if [[ -e "${wildcardlist}" ]]; then - # Determine all subdomains, domain and TLDs - mapfile -t wildcards <<< "$(processWildcards "${domainQuery}")" - for match in "${wildcards[@]}"; do - # Search wildcard list for matches - mapfile -t results <<< "$(scanList "${match}" "${wildcardlist}" "wc")" - if [[ -n "${results[*]}" ]]; then - if [[ -z "${wcMatch:-}" ]] && [[ -z "${blockpage}" ]]; then +scanRegexDatabaseTable() { + local domain list + domain="${1}" + list="${2}" + type="${3:-}" + + # Query all regex from the corresponding database tables + mapfile -t regexList < <(sqlite3 "${gravityDBfile}" "SELECT domain FROM domainlist WHERE type = ${type}" 2> /dev/null) + + # If we have regexps to process + if [[ "${#regexList[@]}" -ne 0 ]]; then + # Split regexps over a new line + str_regexList=$(printf '%s\n' "${regexList[@]}") + # Check domain against regexps + mapfile -t regexMatches < <(scanList "${domain}" "${str_regexList}" "regex") + # If there were regex matches + if [[ "${#regexMatches[@]}" -ne 0 ]]; then + # Split matching regexps over a new line + str_regexMatches=$(printf '%s\n' "${regexMatches[@]}") + # Form a "matched" message + str_message="${matchType^} found in ${COL_BOLD}regex ${list}${COL_NC}" + # Form a "results" message + str_result="${COL_BOLD}${str_regexMatches}${COL_NC}" + # If we are displaying more than just the source of the block + if [[ -z "${blockpage}" ]]; then + # Set the wildcard match flag wcMatch=true - echo " ${matchType^} found in ${COL_BOLD}Wildcards${COL_NC}:" + # Echo the "matched" message, indented by one space + echo " ${str_message}" + # Echo the "results" message, each line indented by three spaces + # shellcheck disable=SC2001 + echo "${str_result}" | sed 's/^/ /' + else + echo "π .wildcard" + exit 0 fi - case "${blockpage}" in - true ) echo "π ${wildcardlist##*/}"; exit 0;; - * ) echo " *.${match}";; - esac fi - done -fi + fi +} -# Get version sorted *.domains filenames (without dir path) -lists=("$(cd "$piholeDir" || exit 0; printf "%s\\n" -- *.domains | sort -V)") +# Scan Whitelist and Blacklist +scanDatabaseTable "${domainQuery}" "whitelist" "0" +scanDatabaseTable "${domainQuery}" "blacklist" "1" -# Query blocklists for occurences of domain -mapfile -t results <<< "$(scanList "${domainQuery}" "${lists[*]}" "${exact}")" +# Scan Regex table +scanRegexDatabaseTable "${domainQuery}" "whitelist" "2" +scanRegexDatabaseTable "${domainQuery}" "blacklist" "3" -# Remove unwanted content from $results -# Each line in $results is formatted as such: [fileName]:[line] -# 1. Delete lines starting with # -# 2. Remove comments after domain -# 3. Remove hosts format IP address -# 4. Remove any lines that no longer contain the queried domain name (in case the matched domain name was in a comment) -esc_domain="${domainQuery//./\\.}" -mapfile -t results <<< "$(IFS=$'\n'; sed \ - -e "/:#/d" \ - -e "s/[ \\t]#.*//g" \ - -e "s/:.*[ \\t]/:/g" \ - -e "/${esc_domain}/!d" \ - <<< "${results[*]}")" +# Query block lists +mapfile -t results <<< "$(scanDatabaseTable "${domainQuery}" "gravity")" # Handle notices if [[ -z "${wbMatch:-}" ]] && [[ -z "${wcMatch:-}" ]] && [[ -z "${results[*]}" ]]; then @@ -184,15 +209,6 @@ elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 100 ]]; then exit 0 fi -# Get adlist file content as array -if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then - for adlistUrl in $(< "${adListsList}"); do - if [[ "${adlistUrl:0:4}" =~ (http|www.) ]]; then - adlists+=("${adlistUrl}") - fi - done -fi - # Print "Exact matches for" title if [[ -n "${exact}" ]] && [[ -z "${blockpage}" ]]; then plural=""; [[ "${#results[*]}" -gt 1 ]] && plural="es" @@ -200,28 +216,25 @@ if [[ -n "${exact}" ]] && [[ -z "${blockpage}" ]]; then fi for result in "${results[@]}"; do - fileName="${result/:*/}" - - # Determine *.domains URL using filename's number - if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then - fileNum="${fileName/list./}"; fileNum="${fileNum%%.*}" - fileName="${adlists[$fileNum]}" - - # Discrepency occurs when adlists has been modified, but Gravity has not been run - if [[ -z "${fileName}" ]]; then - fileName="${COL_LIGHT_RED}(no associated adlists URL found)${COL_NC}" - fi + match="${result/|*/}" + extra="${result#*|}" + adlistAddress="${extra/|*/}" + extra="${extra#*|}" + if [[ "${extra}" == "0" ]]; then + extra="(disabled)" + else + extra="" fi if [[ -n "${blockpage}" ]]; then - echo "${fileNum} ${fileName}" + echo "0 ${adlistAddress}" elif [[ -n "${exact}" ]]; then - echo " ${fileName}" + echo " - ${adlistAddress} ${extra}" else - if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then + if [[ ! "${adlistAddress}" == "${adlistAddress_prev:-}" ]]; then count="" - echo " ${matchType^} found in ${COL_BOLD}${fileName}${COL_NC}:" - fileName_prev="${fileName}" + echo " ${matchType^} found in ${COL_BOLD}${adlistAddress}${COL_NC}:" + adlistAddress_prev="${adlistAddress}" fi : $((count++)) @@ -231,7 +244,7 @@ for result in "${results[@]}"; do [[ "${count}" -gt "${max_count}" ]] && continue echo " ${COL_GRAY}Over ${count} results found, skipping rest of file${COL_NC}" else - echo " ${result#*:}" + echo " ${match} ${extra}" fi fi done diff --git a/advanced/Scripts/updatecheck.sh b/advanced/Scripts/updatecheck.sh index 26dc2ac2..afb03ebb 100755 --- a/advanced/Scripts/updatecheck.sh +++ b/advanced/Scripts/updatecheck.sh @@ -51,6 +51,7 @@ if [[ "$2" == "remote" ]]; then GITHUB_CORE_VERSION="$(json_extract tag_name "$(curl -s 'https://api.github.com/repos/pi-hole/pi-hole/releases/latest' 2> /dev/null)")" echo -n "${GITHUB_CORE_VERSION}" > "${GITHUB_VERSION_FILE}" + chmod 644 "${GITHUB_VERSION_FILE}" if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then GITHUB_WEB_VERSION="$(json_extract tag_name "$(curl -s 'https://api.github.com/repos/pi-hole/AdminLTE/releases/latest' 2> /dev/null)")" @@ -66,6 +67,7 @@ else CORE_BRANCH="$(get_local_branch /etc/.pihole)" echo -n "${CORE_BRANCH}" > "${LOCAL_BRANCH_FILE}" + chmod 644 "${LOCAL_BRANCH_FILE}" if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then WEB_BRANCH="$(get_local_branch /var/www/html/admin)" @@ -79,6 +81,7 @@ else CORE_VERSION="$(get_local_version /etc/.pihole)" echo -n "${CORE_VERSION}" > "${LOCAL_VERSION_FILE}" + chmod 644 "${LOCAL_VERSION_FILE}" if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then WEB_VERSION="$(get_local_version /var/www/html/admin)" diff --git a/advanced/Scripts/webpage.sh b/advanced/Scripts/webpage.sh index 3b17b6b5..829ba57b 100755 --- a/advanced/Scripts/webpage.sh +++ b/advanced/Scripts/webpage.sh @@ -17,6 +17,9 @@ readonly FTLconf="/etc/pihole/pihole-FTL.conf" # 03 -> wildcards readonly dhcpstaticconfig="/etc/dnsmasq.d/04-pihole-static-dhcp.conf" readonly PI_HOLE_BIN_DIR="/usr/local/bin" +readonly dnscustomfile="/etc/pihole/custom.list" + +readonly gravityDBfile="/etc/pihole/gravity.db" coltable="/opt/pihole/COL_TABLE" if [[ -f ${coltable} ]]; then @@ -86,9 +89,9 @@ SetTemperatureUnit() { HashPassword() { # Compute password hash twice to avoid rainbow table vulnerability - return=$(echo -n ${1} | sha256sum | sed 's/\s.*$//') - return=$(echo -n ${return} | sha256sum | sed 's/\s.*$//') - echo ${return} + return=$(echo -n "${1}" | sha256sum | sed 's/\s.*$//') + return=$(echo -n "${return}" | sha256sum | sed 's/\s.*$//') + echo "${return}" } SetWebPassword() { @@ -142,18 +145,18 @@ ProcessDNSSettings() { delete_dnsmasq_setting "server" COUNTER=1 - while [[ 1 ]]; do + while true ; do var=PIHOLE_DNS_${COUNTER} if [ -z "${!var}" ]; then break; fi add_dnsmasq_setting "server" "${!var}" - let COUNTER=COUNTER+1 + (( COUNTER++ )) done # The option LOCAL_DNS_PORT is deprecated # We apply it once more, and then convert it into the current format - if [ ! -z "${LOCAL_DNS_PORT}" ]; then + if [ -n "${LOCAL_DNS_PORT}" ]; then add_dnsmasq_setting "server" "127.0.0.1#${LOCAL_DNS_PORT}" add_setting "PIHOLE_DNS_${COUNTER}" "127.0.0.1#${LOCAL_DNS_PORT}" delete_setting "LOCAL_DNS_PORT" @@ -183,7 +186,7 @@ trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC68345710423 delete_dnsmasq_setting "host-record" - if [ ! -z "${HOSTRECORD}" ]; then + if [ -n "${HOSTRECORD}" ]; then add_dnsmasq_setting "host-record" "${HOSTRECORD}" fi @@ -328,6 +331,7 @@ dhcp-option=option:router,${DHCP_ROUTER} dhcp-leasefile=/etc/pihole/dhcp.leases #quiet-dhcp " > "${dhcpconfig}" + chmod 644 "${dhcpconfig}" if [[ "${PIHOLE_DOMAIN}" != "none" ]]; then echo "domain=${PIHOLE_DOMAIN}" >> "${dhcpconfig}" @@ -399,19 +403,19 @@ SetWebUILayout() { } CustomizeAdLists() { - list="/etc/pihole/adlists.list" + local address + address="${args[3]}" + local comment + comment="${args[4]}" if [[ "${args[2]}" == "enable" ]]; then - sed -i "\\@${args[3]}@s/^#http/http/g" "${list}" + sqlite3 "${gravityDBfile}" "UPDATE adlist SET enabled = 1 WHERE address = '${address}'" elif [[ "${args[2]}" == "disable" ]]; then - sed -i "\\@${args[3]}@s/^http/#http/g" "${list}" + sqlite3 "${gravityDBfile}" "UPDATE adlist SET enabled = 0 WHERE address = '${address}'" elif [[ "${args[2]}" == "add" ]]; then - if [[ $(grep -c "^${args[3]}$" "${list}") -eq 0 ]] ; then - echo "${args[3]}" >> ${list} - fi + sqlite3 "${gravityDBfile}" "INSERT OR IGNORE INTO adlist (address, comment) VALUES ('${address}', '${comment}')" elif [[ "${args[2]}" == "del" ]]; then - var=$(echo "${args[3]}" | sed 's/\//\\\//g') - sed -i "/${var}/Id" "${list}" + sqlite3 "${gravityDBfile}" "DELETE FROM adlist WHERE address = '${address}'" else echo "Not permitted" return 1 @@ -523,10 +527,10 @@ Interfaces: fi if [[ "${args[2]}" == "all" ]]; then - echo -e " ${INFO} Listening on all interfaces, permiting all origins. Please use a firewall!" + echo -e " ${INFO} Listening on all interfaces, permitting all origins. Please use a firewall!" change_setting "DNSMASQ_LISTENING" "all" elif [[ "${args[2]}" == "local" ]]; then - echo -e " ${INFO} Listening on all interfaces, permiting origins from one hop away (LAN)" + echo -e " ${INFO} Listening on all interfaces, permitting origins from one hop away (LAN)" change_setting "DNSMASQ_LISTENING" "local" else echo -e " ${INFO} Listening only on interface ${PIHOLE_INTERFACE}" @@ -543,23 +547,50 @@ Interfaces: } Teleporter() { - local datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S") + local datetimestamp + datetimestamp=$(date "+%Y-%m-%d_%H-%M-%S") php /var/www/html/admin/scripts/pi-hole/php/teleporter.php > "pi-hole-teleporter_${datetimestamp}.tar.gz" } +checkDomain() +{ + local domain validDomain + # Convert to lowercase + domain="${1,,}" + validDomain=$(grep -P "^((-|_)*[a-z\\d]((-|_)*[a-z\\d])*(-|_)*)(\\.(-|_)*([a-z\\d]((-|_)*[a-z\\d])*))*$" <<< "${domain}") # Valid chars check + validDomain=$(grep -P "^[^\\.]{1,63}(\\.[^\\.]{1,63})*$" <<< "${validDomain}") # Length of each label + echo "${validDomain}" +} + addAudit() { shift # skip "-a" shift # skip "audit" - for var in "$@" + local domains validDomain + domains="" + for domain in "$@" do - echo "${var}" >> /etc/pihole/auditlog.list + # Check domain to be added. Only continue if it is valid + validDomain="$(checkDomain "${domain}")" + if [[ -n "${validDomain}" ]]; then + # Put comma in between domains when there is + # more than one domains to be added + # SQL INSERT allows adding multiple rows at once using the format + ## INSERT INTO table (domain) VALUES ('abc.de'),('fgh.ij'),('klm.no'),('pqr.st'); + if [[ -n "${domains}" ]]; then + domains="${domains}," + fi + domains="${domains}('${domain}')" + fi done + # Insert only the domain here. The date_added field will be + # filled with its default value (date_added = current timestamp) + sqlite3 "${gravityDBfile}" "INSERT INTO domain_audit (domain) VALUES ${domains};" } clearAudit() { - echo -n "" > /etc/pihole/auditlog.list + sqlite3 "${gravityDBfile}" "DELETE FROM domain_audit;" } SetPrivacyLevel() { @@ -569,6 +600,28 @@ SetPrivacyLevel() { fi } +AddCustomDNSAddress() { + echo -e " ${TICK} Adding custom DNS entry..." + + ip="${args[2]}" + host="${args[3]}" + echo "${ip} ${host}" >> "${dnscustomfile}" + + # Restart dnsmasq to load new custom DNS entries + RestartDNS +} + +RemoveCustomDNSAddress() { + echo -e " ${TICK} Removing custom DNS entry..." + + ip="${args[2]}" + host="${args[3]}" + sed -i "/${ip} ${host}/d" "${dnscustomfile}" + + # Restart dnsmasq to update removed custom DNS entries + RestartDNS +} + main() { args=("$@") @@ -600,6 +653,8 @@ main() { "audit" ) addAudit "$@";; "clearaudit" ) clearAudit;; "-l" | "privacylevel" ) SetPrivacyLevel;; + "addcustomdns" ) AddCustomDNSAddress;; + "removecustomdns" ) RemoveCustomDNSAddress;; * ) helpFunc;; esac diff --git a/advanced/Scripts/wildcard_regex_converter.sh b/advanced/Scripts/wildcard_regex_converter.sh index 8c9578a3..b4b6b4a1 100644 --- a/advanced/Scripts/wildcard_regex_converter.sh +++ b/advanced/Scripts/wildcard_regex_converter.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. diff --git a/advanced/Templates/gravity.db.sql b/advanced/Templates/gravity.db.sql new file mode 100644 index 00000000..e543bd19 --- /dev/null +++ b/advanced/Templates/gravity.db.sql @@ -0,0 +1,188 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; + +CREATE TABLE "group" +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled BOOLEAN NOT NULL DEFAULT 1, + name TEXT UNIQUE NOT NULL, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + description TEXT +); +INSERT INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + +CREATE TABLE domainlist +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type INTEGER NOT NULL DEFAULT 0, + domain TEXT UNIQUE NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 1, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + comment TEXT +); + +CREATE TABLE adlist +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + address TEXT UNIQUE NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT 1, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + comment TEXT +); + +CREATE TABLE adlist_by_group +( + adlist_id INTEGER NOT NULL REFERENCES adlist (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (adlist_id, group_id) +); + +CREATE TABLE gravity +( + domain TEXT NOT NULL, + adlist_id INTEGER NOT NULL REFERENCES adlist (id) +); + +CREATE TABLE info +( + property TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +INSERT INTO "info" VALUES('version','11'); + +CREATE TABLE domain_audit +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT UNIQUE NOT NULL, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)) +); + +CREATE TABLE domainlist_by_group +( + domainlist_id INTEGER NOT NULL REFERENCES domainlist (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (domainlist_id, group_id) +); + +CREATE TABLE client +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip TEXT NOL NULL UNIQUE, + date_added INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + date_modified INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), + comment TEXT +); + +CREATE TABLE client_by_group +( + client_id INTEGER NOT NULL REFERENCES client (id), + group_id INTEGER NOT NULL REFERENCES "group" (id), + PRIMARY KEY (client_id, group_id) +); + +CREATE TRIGGER tr_adlist_update AFTER UPDATE ON adlist + BEGIN + UPDATE adlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE address = NEW.address; + END; + +CREATE TRIGGER tr_client_update AFTER UPDATE ON client + BEGIN + UPDATE client SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE ip = NEW.ip; + END; + +CREATE TRIGGER tr_domainlist_update AFTER UPDATE ON domainlist + BEGIN + UPDATE domainlist SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE domain = NEW.domain; + END; + +CREATE VIEW vw_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 0 + ORDER BY domainlist.id; + +CREATE VIEW vw_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 1 + ORDER BY domainlist.id; + +CREATE VIEW vw_regex_whitelist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 2 + ORDER BY domainlist.id; + +CREATE VIEW vw_regex_blacklist AS SELECT domain, domainlist.id AS id, domainlist_by_group.group_id AS group_id + FROM domainlist + LEFT JOIN domainlist_by_group ON domainlist_by_group.domainlist_id = domainlist.id + LEFT JOIN "group" ON "group".id = domainlist_by_group.group_id + WHERE domainlist.enabled = 1 AND (domainlist_by_group.group_id IS NULL OR "group".enabled = 1) + AND domainlist.type = 3 + ORDER BY domainlist.id; + +CREATE VIEW vw_gravity AS SELECT domain, adlist_by_group.group_id AS group_id + FROM gravity + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = gravity.adlist_id + LEFT JOIN adlist ON adlist.id = gravity.adlist_id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1); + +CREATE VIEW vw_adlist AS SELECT DISTINCT address, adlist.id AS id + FROM adlist + LEFT JOIN adlist_by_group ON adlist_by_group.adlist_id = adlist.id + LEFT JOIN "group" ON "group".id = adlist_by_group.group_id + WHERE adlist.enabled = 1 AND (adlist_by_group.group_id IS NULL OR "group".enabled = 1) + ORDER BY adlist.id; + +CREATE TRIGGER tr_domainlist_add AFTER INSERT ON domainlist + BEGIN + INSERT INTO domainlist_by_group (domainlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_client_add AFTER INSERT ON client + BEGIN + INSERT INTO client_by_group (client_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_adlist_add AFTER INSERT ON adlist + BEGIN + INSERT INTO adlist_by_group (adlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_group_update AFTER UPDATE ON "group" + BEGIN + UPDATE "group" SET date_modified = (cast(strftime('%s', 'now') as int)) WHERE id = NEW.id; + END; + +CREATE TRIGGER tr_group_zero AFTER DELETE ON "group" + BEGIN + INSERT OR IGNORE INTO "group" (id,enabled,name) VALUES (0,1,'Unassociated'); + END; + +CREATE TRIGGER tr_domainlist_delete AFTER DELETE ON domainlist + BEGIN + DELETE FROM domainlist_by_group WHERE domainlist_id = OLD.id; + END; + +CREATE TRIGGER tr_adlist_delete AFTER DELETE ON adlist + BEGIN + DELETE FROM adlist_by_group WHERE adlist_id = OLD.id; + END; + +CREATE TRIGGER tr_client_delete AFTER DELETE ON client + BEGIN + DELETE FROM client_by_group WHERE client_id = OLD.id; + END; + +COMMIT; diff --git a/advanced/Templates/gravity_copy.sql b/advanced/Templates/gravity_copy.sql new file mode 100644 index 00000000..4a2a9b22 --- /dev/null +++ b/advanced/Templates/gravity_copy.sql @@ -0,0 +1,42 @@ +.timeout 30000 + +ATTACH DATABASE '/etc/pihole/gravity.db' AS OLD; + +BEGIN TRANSACTION; + +DROP TRIGGER tr_domainlist_add; +DROP TRIGGER tr_client_add; +DROP TRIGGER tr_adlist_add; + +INSERT OR REPLACE INTO "group" SELECT * FROM OLD."group"; +INSERT OR REPLACE INTO domain_audit SELECT * FROM OLD.domain_audit; + +INSERT OR REPLACE INTO domainlist SELECT * FROM OLD.domainlist; +INSERT OR REPLACE INTO domainlist_by_group SELECT * FROM OLD.domainlist_by_group; + +INSERT OR REPLACE INTO adlist SELECT * FROM OLD.adlist; +INSERT OR REPLACE INTO adlist_by_group SELECT * FROM OLD.adlist_by_group; + +INSERT OR REPLACE INTO info SELECT * FROM OLD.info; + +INSERT OR REPLACE INTO client SELECT * FROM OLD.client; +INSERT OR REPLACE INTO client_by_group SELECT * FROM OLD.client_by_group; + + +CREATE TRIGGER tr_domainlist_add AFTER INSERT ON domainlist + BEGIN + INSERT INTO domainlist_by_group (domainlist_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_client_add AFTER INSERT ON client + BEGIN + INSERT INTO client_by_group (client_id, group_id) VALUES (NEW.id, 0); + END; + +CREATE TRIGGER tr_adlist_add AFTER INSERT ON adlist + BEGIN + INSERT INTO adlist_by_group (adlist_id, group_id) VALUES (NEW.id, 0); + END; + + +COMMIT; diff --git a/advanced/Templates/pihole-FTL.service b/advanced/Templates/pihole-FTL.service index 8a4c7ce6..f32a5e89 100644 --- a/advanced/Templates/pihole-FTL.service +++ b/advanced/Templates/pihole-FTL.service @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ### BEGIN INIT INFO # Provides: pihole-FTL # Required-Start: $remote_fs $syslog @@ -48,7 +48,8 @@ start() { chown pihole:pihole /etc/pihole /etc/pihole/dhcp.leases 2> /dev/null chown pihole:pihole /var/log/pihole-FTL.log /var/log/pihole.log chmod 0644 /var/log/pihole-FTL.log /run/pihole-FTL.pid /run/pihole-FTL.port /var/log/pihole.log - echo "nameserver 127.0.0.1" | /sbin/resolvconf -a lo.piholeFTL + # Chown database files to the user FTL runs as. We ignore errors as the files may not (yet) exist + chown pihole:pihole /etc/pihole/pihole-FTL.db /etc/pihole/gravity.db 2> /dev/null if setcap CAP_NET_BIND_SERVICE,CAP_NET_RAW,CAP_NET_ADMIN+eip "$(which pihole-FTL)"; then su -s /bin/sh -c "/usr/bin/pihole-FTL" "$FTLUSER" else @@ -62,7 +63,6 @@ start() { # Stop the service stop() { if is_running; then - /sbin/resolvconf -d lo.piholeFTL kill "$(get_pid)" for i in {1..5}; do if ! is_running; then diff --git a/advanced/bash-completion/pihole b/advanced/bash-completion/pihole index 7ba0dad8..cea36060 100644 --- a/advanced/bash-completion/pihole +++ b/advanced/bash-completion/pihole @@ -7,7 +7,7 @@ _pihole() { case "${prev}" in "pihole") - opts="admin blacklist checkout chronometer debug disable enable flush help logging query reconfigure regex restartdns status tail uninstall updateGravity updatePihole version wildcard whitelist" + opts="admin blacklist checkout chronometer debug disable enable flush help logging query reconfigure regex restartdns status tail uninstall updateGravity updatePihole version wildcard whitelist arpflush" COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) ;; "whitelist"|"blacklist"|"wildcard"|"regex") diff --git a/advanced/index.php b/advanced/index.php index 78135e1a..b0c4a7c3 100644 --- a/advanced/index.php +++ b/advanced/index.php @@ -96,26 +96,30 @@ if ($serverName === "pi.hole") { // Define admin email address text based off $svEmail presence $bpAskAdmin = !empty($svEmail) ? '' : ""; -// Determine if at least one block list has been generated -$blocklistglob = glob("/etc/pihole/list.0.*.domains"); -if ($blocklistglob === array()) { - die("[ERROR] There are no domain lists generated lists within /etc/pihole/! Please update gravity by running pihole -g, or repair Pi-hole using pihole -r."); -} - -// Set location of adlists file -if (is_file("/etc/pihole/adlists.list")) { - $adLists = "/etc/pihole/adlists.list"; -} elseif (is_file("/etc/pihole/adlists.default")) { - $adLists = "/etc/pihole/adlists.default"; +// Get possible non-standard location of FTL's database +$FTLsettings = parse_ini_file("/etc/pihole/pihole-FTL.conf"); +if (isset($FTLsettings["GRAVITYDB"])) { + $gravityDBFile = $FTLsettings["GRAVITYDB"]; } else { - die("[ERROR] File not found: /etc/pihole/adlists.list"); + $gravityDBFile = "/etc/pihole/gravity.db"; } -// Get all URLs starting with "http" or "www" from adlists and re-index array numerically -$adlistsUrls = array_values(preg_grep("/(^http)|(^www)/i", file($adLists, FILE_IGNORE_NEW_LINES))); +// Connect to gravity.db +try { + $db = new SQLite3($gravityDBFile, SQLITE3_OPEN_READONLY); +} catch (Exception $exception) { + die("[ERROR]: Failed to connect to gravity.db"); +} + +// Get all adlist addresses +$adlistResults = $db->query("SELECT address FROM vw_adlist"); +$adlistsUrls = array(); +while ($row = $adlistResults->fetchArray()) { + array_push($adlistsUrls, $row[0]); +} if (empty($adlistsUrls)) - die("[ERROR]: There are no adlist URL's found within $adLists"); + die("[ERROR]: There are no adlists enabled"); // Get total number of blocklists (Including Whitelist, Blacklist & Wildcard lists) $adlistsCount = count($adlistsUrls) + 3; diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index ee4793e7..65c72b40 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -70,7 +70,6 @@ PI_HOLE_BLOCKPAGE_DIR="${webroot}/pihole" useUpdateVars=false adlistFile="/etc/pihole/adlists.list" -regexFile="/etc/pihole/regex.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="" @@ -124,7 +123,7 @@ done # If the color table file exists, if [[ -f "${coltable}" ]]; then # source it - source ${coltable} + source "${coltable}" # Otherwise, else # Set these values so the installer can still run in color @@ -185,26 +184,26 @@ if is_command apt-get ; then # 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) + 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 - # on Ubuntu 18.04.1 LTS we need to add the universe repository to gain access to dialog and dhcpcd5 + # on Ubuntu 18.04.1 LTS we need to add the universe repository to gain access to dhcpcd5 APT_SOURCES="/etc/apt/sources.list" if awk 'BEGIN{a=1;b=0}/bionic main/{a=0}/bionic.*universe/{b=1}END{exit a + b}' ${APT_SOURCES}; then - if ! whiptail --defaultno --title "Dependencies Require Update to Allowed Repositories" --yesno "Would you like to enable 'universe' repository?\\n\\nThis repository is required by the following packages:\\n\\n- dhcpcd5\\n- dialog" ${r} ${c}; then + if ! whiptail --defaultno --title "Dependencies Require Update to Allowed Repositories" --yesno "Would you like to enable 'universe' repository?\\n\\nThis repository is required by the following packages:\\n\\n- dhcpcd5" "${r}" "${c}"; then printf " %b Aborting installation: dependencies could not be installed.\\n" "${CROSS}" exit # exit the installer else printf " %b Enabling universe package repository for Ubuntu Bionic\\n" "${INFO}" - cp ${APT_SOURCES} ${APT_SOURCES}.backup # Backup current repo list + cp -p ${APT_SOURCES} ${APT_SOURCES}.backup # Backup current repo list printf " %b Backed up current configuration to %s\\n" "${TICK}" "${APT_SOURCES}.backup" add-apt-repository universe printf " %b Enabled %s\\n" "${TICK}" "'universe' repository" fi fi # 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 + if "${PKG_MANAGER}" install --dry-run iproute2 > /dev/null 2>&1; then # we can install it iproute_pkg="iproute2" # Otherwise, @@ -225,7 +224,7 @@ if is_command apt-get ; then # 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 + if "${PKG_MANAGER}" install --dry-run php > /dev/null 2>&1; then phpVer="php" # fall back on the php5 packages else @@ -236,19 +235,19 @@ if is_command apt-get ; then 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 + 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) + INSTALLER_DEPS=(dhcpcd5 git "${iproute_pkg}" whiptail) # Pi-hole itself has several dependencies that also need to be installed PIHOLE_DEPS=(cron curl dnsutils iputils-ping lsof netcat psmisc sudo unzip wget idn2 sqlite3 libcap2-bin dns-root-data resolvconf libcap2) # 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}) + PIHOLE_WEB_DEPS=(lighttpd "${phpVer}-common" "${phpVer}-cgi" "${phpVer}-${phpSqlite}" "${phpVer}-xml" "php-intl") # The Web server user, LIGHTTPD_USER="www-data" # group, @@ -284,19 +283,18 @@ elif is_command rpm ; then # Fedora and family update cache on every PKG_INSTALL call, no need for a separate update. UPDATE_PKG_CACHE=":" - PKG_INSTALL=(${PKG_MANAGER} install -y) + PKG_INSTALL=("${PKG_MANAGER}" install -y) PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l" - INSTALLER_DEPS=(dialog git iproute newt procps-ng which chkconfig) + INSTALLER_DEPS=(git iproute newt procps-ng which chkconfig) PIHOLE_DEPS=(bind-utils cronie curl findutils nmap-ncat sudo unzip wget libidn2 psmisc sqlite libcap) - PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php-common php-cli php-pdo) + 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 Fedora, if grep -qiE 'fedora|fedberry' /etc/redhat-release; then # all required packages should be available by default with the latest fedora release - # ensure 'php-json' is installed on Fedora (installed as dependency on CentOS7 + Remi repository) - PIHOLE_WEB_DEPS+=('php-json') + : # continue # or if host OS is CentOS, elif grep -qiE 'centos|scientific' /etc/redhat-release; then # Pi-Hole currently supports CentOS 7+ with PHP7+ @@ -311,7 +309,21 @@ elif is_command rpm ; then # exit the installer exit fi - # on CentOS we need to add the EPEL repository to gain access to Fedora packages + # php-json is not required on CentOS 7 as it is already compiled into php + # verifiy via `php -m | grep json` + if [[ $CURRENT_CENTOS_VERSION -eq 7 ]]; then + # create a temporary array as arrays are not designed for use as mutable data structures + CENTOS7_PIHOLE_WEB_DEPS=() + for i in "${!PIHOLE_WEB_DEPS[@]}"; do + if [[ ${PIHOLE_WEB_DEPS[i]} != "php-json" ]]; then + CENTOS7_PIHOLE_WEB_DEPS+=( "${PIHOLE_WEB_DEPS[i]}" ) + fi + done + # re-assign the clean dependency array back to PIHOLE_WEB_DEPS + PIHOLE_WEB_DEPS=("${CENTOS7_PIHOLE_WEB_DEPS[@]}") + unset CENTOS7_PIHOLE_WEB_DEPS + fi + # CentOS requires 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 @@ -322,7 +334,7 @@ elif is_command rpm ; then # 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) + 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 @@ -332,7 +344,7 @@ elif is_command rpm ; then 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 + 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 printf " %b User opt-out of PHP 7 upgrade on CentOS. Deprecated PHP may be in use.\\n" "${INFO}" : # continue with unsupported php version @@ -355,7 +367,7 @@ elif is_command rpm ; then fi else # Warn user of unsupported version of Fedora or CentOS - if ! whiptail --defaultno --title "Unsupported RPM based distribution" --yesno "Would you like to continue installation on an unsupported RPM based distribution?\\n\\nPlease ensure the following packages have been installed manually:\\n\\n- lighttpd\\n- lighttpd-fastcgi\\n- PHP version 7+" ${r} ${c}; then + if ! whiptail --defaultno --title "Unsupported RPM based distribution" --yesno "Would you like to continue installation on an unsupported RPM based distribution?\\n\\nPlease ensure the following packages have been installed manually:\\n\\n- lighttpd\\n- lighttpd-fastcgi\\n- PHP version 7+" "${r}" "${c}"; then printf " %b Aborting installation due to unsupported RPM based distribution\\n" "${CROSS}" exit # exit the installer else @@ -377,16 +389,12 @@ 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}" + 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=$? @@ -396,7 +404,7 @@ is_repo() { rc=1 fi # Move back into the directory the user started in - cd "${curdir}" + popd &> /dev/null || return 1 # Return the code; if one is not set, return 0 return "${rc:-0}" } @@ -406,6 +414,7 @@ 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 @@ -417,9 +426,21 @@ make_repo() { fi # Clone the repo and return the return code from this command git clone -q --depth 20 "${remoteRepo}" "${directory}" &> /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}" + # 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 availible 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}" - # Always return 0? Not sure this is correct + + # Move back into the original directory + popd &> /dev/null || return 1 return 0 } @@ -430,17 +451,14 @@ update_repo() { # 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 + 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}" - - # 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 + 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 @@ -448,10 +466,18 @@ update_repo() { git clean --quiet --force -d || true # Okay for already clean directory # Pull the latest commits git pull --quiet &> /dev/null || return $? + # Check current branch. If it is master, then reset to the latest availible 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 - cd "${curdir}" &> /dev/null || return 1 + popd &> /dev/null || return 1 return 0 } @@ -490,15 +516,19 @@ resetRepo() { # Use named variables for arguments local directory="${1}" # Move into the directory - cd "${directory}" &> /dev/null || return 1 + 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 # Returning success anyway? return 0 } @@ -540,15 +570,15 @@ get_available_interfaces() { # 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} + 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} + 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} +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 @@ -635,7 +665,7 @@ chooseInterface() { # Feed the available interfaces into this while loop done <<< "${availableInterfaces}" # The whiptail command that will be run, stored in a variable - chooseInterfaceCmd=(whiptail --separate-output --radiolist "Choose An Interface (press space to select)" ${r} ${c} ${interfaceCount}) + chooseInterfaceCmd=(whiptail --separate-output --radiolist "Choose An Interface (press space to select)" "${r}" "${c}" "${interfaceCount}") # Now run the command using the interfaces saved into the array chooseInterfaceOptions=$("${chooseInterfaceCmd[@]}" "${interfacesArray[@]}" 2>&1 >/dev/tty) || \ # If the user chooses Cancel, exit @@ -716,7 +746,7 @@ useIPv6dialog() { # If the IPV6_ADDRESS contains a value if [[ ! -z "${IPV6_ADDRESS}" ]]; then # Display that IPv6 is supported and will be used - whiptail --msgbox --backtitle "IPv6..." --title "IPv6 Supported" "$IPV6_ADDRESS will be used to block ads." ${r} ${c} + whiptail --msgbox --backtitle "IPv6..." --title "IPv6 Supported" "$IPV6_ADDRESS will be used to block ads." "${r}" "${c}" fi } @@ -726,7 +756,7 @@ use4andor6() { local useIPv4 local useIPv6 # Let use select IPv4 and/or IPv6 via a checklist - cmd=(whiptail --separate-output --checklist "Select Protocols (press space to select)" ${r} ${c} 2) + cmd=(whiptail --separate-output --checklist "Select Protocols (press space to select)" "${r}" "${c}" 2) # In an array, show the options available: # IPv4 (on by default) options=(IPv4 "Block ads over IPv4" on @@ -775,11 +805,11 @@ getStaticIPv4Settings() { # This is useful for users that are using DHCP reservations; then we can just use the information gathered via our functions if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Do you want to use your current network settings as a static address? IP address: ${IPV4_ADDRESS} - Gateway: ${IPv4gw}" ${r} ${c}; then + Gateway: ${IPv4gw}" "${r}" "${c}"; then # If they choose yes, let the user know that the IP address will not be available via DHCP and may cause a conflict. whiptail --msgbox --backtitle "IP information" --title "FYI: IP Conflict" "It is possible your router could still try to assign this IP to a device, which would cause a conflict. But in most cases the router is smart enough to not do that. If you are worried, either manually set the address, or modify the DHCP reservation pool so it does not include the IP you want. -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} +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}" # Nothing else to do since the variables are already set above else # Otherwise, we need to ask the user to input their desired settings. @@ -788,13 +818,13 @@ It is also possible to use a DHCP reservation, but if you are going to do that, until [[ "${ipSettingsCorrect}" = True ]]; do # Ask for the IPv4 address - IPV4_ADDRESS=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 address" --inputbox "Enter your desired IPv4 address" ${r} ${c} "${IPV4_ADDRESS}" 3>&1 1>&2 2>&3) || \ + IPV4_ADDRESS=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 address" --inputbox "Enter your desired IPv4 address" "${r}" "${c}" "${IPV4_ADDRESS}" 3>&1 1>&2 2>&3) || \ # Cancelling IPv4 settings window { ipSettingsCorrect=False; echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; } printf " %b Your static IPv4 address: %s\\n" "${INFO}" "${IPV4_ADDRESS}" # Ask for the gateway - IPv4gw=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 gateway (router)" --inputbox "Enter your desired IPv4 default gateway" ${r} ${c} "${IPv4gw}" 3>&1 1>&2 2>&3) || \ + IPv4gw=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 gateway (router)" --inputbox "Enter your desired IPv4 default gateway" "${r}" "${c}" "${IPv4gw}" 3>&1 1>&2 2>&3) || \ # Cancelling gateway settings window { ipSettingsCorrect=False; echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; } printf " %b Your static IPv4 gateway: %s\\n" "${INFO}" "${IPv4gw}" @@ -802,7 +832,7 @@ It is also possible to use a DHCP reservation, but if you are going to do that, # Give the user a chance to review their settings before moving on if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Are these settings correct? IP address: ${IPV4_ADDRESS} - Gateway: ${IPv4gw}" ${r} ${c}; then + Gateway: ${IPv4gw}" "${r}" "${c}"; then # After that's done, the loop ends and we move on ipSettingsCorrect=True else @@ -829,7 +859,8 @@ setDHCPCD() { # 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 You may need to restart after the install is complete\\n" "${TICK}" "${IPV4_ADDRESS%/*}" + 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 } @@ -850,7 +881,7 @@ setIFCFG() { # Put the IP in variables without the CIDR notation printf -v CIDR "%s" "${IPV4_ADDRESS##*/}" # Backup existing interface configuration: - cp "${IFCFG_FILE}" "${IFCFG_FILE}".pihole.orig + cp -p "${IFCFG_FILE}" "${IFCFG_FILE}".pihole.orig # Build Interface configuration file using the GLOBAL variables we have { echo "# Configured via Pi-hole installer" @@ -864,6 +895,8 @@ setIFCFG() { echo "DNS2=$PIHOLE_DNS_2" echo "USERCTL=no" }> "${IFCFG_FILE}" + chmod 644 "${IFCFG_FILE}" + chown root:root "${IFCFG_FILE}" # Use ip to immediately set the new address ip addr replace dev "${PIHOLE_INTERFACE}" "${IPV4_ADDRESS}" # If NetworkMangler command line interface exists and ready to mangle, @@ -928,7 +961,7 @@ valid_ip() { # and set the new one to a dot (period) IFS='.' # Put the IP into an array - ip=(${ip}) + read -r -a ip <<< "${ip}" # Restore the IFS to what it was IFS=${OIFS} ## Evaluate each octet by checking if it's less than or equal to 255 (the max for each octet) @@ -938,7 +971,7 @@ valid_ip() { stat=$? fi # Return the exit code - return ${stat} + return "${stat}" } # A function to choose the upstream DNS provider(s) @@ -968,13 +1001,11 @@ setDNS() { # Restore the IFS to what it was IFS=${OIFS} # In a whiptail dialog, show the options - DNSchoices=$(whiptail --separate-output --menu "Select Upstream DNS Provider. To use your own, select Custom." ${r} ${c} 7 \ + DNSchoices=$(whiptail --separate-output --menu "Select Upstream DNS Provider. To use your own, select Custom." "${r}" "${c}" 7 \ "${DNSChooseOptions[@]}" 2>&1 >/dev/tty) || \ # exit if Cancel is selected { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } - # Display the selection - printf " %b Using " "${INFO}" # Depending on the user's choice, set the GLOBAl variables to the IP of the respective provider if [[ "${DNSchoices}" == "Custom" ]] then @@ -998,7 +1029,7 @@ setDNS() { fi # Dialog for the user to enter custom upstream servers - piholeDNS=$(whiptail --backtitle "Specify Upstream DNS Provider(s)" --inputbox "Enter your desired upstream DNS provider(s), separated by a comma.\\n\\nFor example '8.8.8.8, 8.8.4.4'" ${r} ${c} "${prePopulate}" 3>&1 1>&2 2>&3) || \ + piholeDNS=$(whiptail --backtitle "Specify Upstream DNS Provider(s)" --inputbox "Enter your desired upstream DNS provider(s), separated by a comma.\\n\\nFor example '8.8.8.8, 8.8.4.4'" "${r}" "${c}" "${prePopulate}" 3>&1 1>&2 2>&3) || \ { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } # Clean user input and replace whitespace with comma. piholeDNS=$(sed 's/[, \t]\+/,/g' <<< "${piholeDNS}") @@ -1026,14 +1057,14 @@ setDNS() { if [[ "${PIHOLE_DNS_2}" == "${strInvalid}" ]]; then PIHOLE_DNS_2="" fi - # Since the settings will not work, stay in the loop - DNSSettingsCorrect=False + # Since the settings will not work, stay in the loop + DNSSettingsCorrect=False # Otherwise, else # Show the settings - if (whiptail --backtitle "Specify Upstream DNS Provider(s)" --title "Upstream DNS Provider(s)" --yesno "Are these settings correct?\\n DNS Server 1: $PIHOLE_DNS_1\\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c}); then - # and break from the loop since the servers are valid - DNSSettingsCorrect=True + if (whiptail --backtitle "Specify Upstream DNS Provider(s)" --title "Upstream DNS Provider(s)" --yesno "Are these settings correct?\\n DNS Server 1: $PIHOLE_DNS_1\\n DNS Server 2: ${PIHOLE_DNS_2}" "${r}" "${c}"); then + # and break from the loop since the servers are valid + DNSSettingsCorrect=True # Otherwise, else # If the settings are wrong, the loop continues @@ -1041,7 +1072,7 @@ setDNS() { fi fi done - else + else # Save the old Internal Field Separator in a variable OIFS=$IFS # and set the new one to newline @@ -1051,7 +1082,6 @@ setDNS() { DNSName="$(cut -d';' -f1 <<< "${DNSServer}")" if [[ "${DNSchoices}" == "${DNSName}" ]] then - printf "%s\\n" "${DNSName}" PIHOLE_DNS_1="$(cut -d';' -f2 <<< "${DNSServer}")" PIHOLE_DNS_2="$(cut -d';' -f3 <<< "${DNSServer}")" break @@ -1060,6 +1090,11 @@ setDNS() { # 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 @@ -1122,7 +1157,7 @@ setAdminFlag() { local WebChoices # Similar to the logging function, ask what the user wants - WebToggleCommand=(whiptail --separate-output --radiolist "Do you wish to install the web admin interface?" ${r} ${c} 6) + WebToggleCommand=(whiptail --separate-output --radiolist "Do you wish to install the web admin interface?" "${r}" "${c}" 6) # with the default being enabled WebChooseOptions=("On (Recommended)" "" on Off "" off) @@ -1187,6 +1222,7 @@ chooseBlocklists() { do appendToListsFile "${choice}" done + chmod 644 "${adlistFile}" } # Accept a string parameter, it must be one of the default lists @@ -1196,7 +1232,7 @@ appendToListsFile() { case $1 in StevenBlack ) echo "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" >> "${adlistFile}";; MalwareDom ) echo "https://mirror1.malwaredomains.com/files/justdomains" >> "${adlistFile}";; - Cameleon ) echo "http://sysctl.org/cameleon/hosts" >> "${adlistFile}";; + Cameleon ) echo "https://sysctl.org/cameleon/hosts" >> "${adlistFile}";; DisconTrack ) echo "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt" >> "${adlistFile}";; DisconAd ) echo "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt" >> "${adlistFile}";; HostsFile ) echo "https://hosts-file.net/ad_servers.txt" >> "${adlistFile}";; @@ -1225,6 +1261,7 @@ version_check_dnsmasq() { 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_snippet="${PI_HOLE_LOCAL_REPO}/advanced/01-pihole.conf" local dnsmasq_pihole_01_location="/etc/dnsmasq.d/01-pihole.conf" @@ -1232,16 +1269,17 @@ version_check_dnsmasq() { # If the dnsmasq config file exists if [[ -f "${dnsmasq_conf}" ]]; then printf " %b Existing dnsmasq.conf found..." "${INFO}" - # If gravity.list is found within this file, we presume it's from older versions on Pi-hole, - if grep -q ${dnsmasq_pihole_id_string} ${dnsmasq_conf}; then + # 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} + 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 - cp ${dnsmasq_original_config} ${dnsmasq_conf} + install -D -m 644 -T "${dnsmasq_original_config}" "${dnsmasq_conf}" printf "%b %b Restoring default dnsmasq.conf...\\n" "${OVER}" "${TICK}" # Otherwise, else @@ -1252,47 +1290,47 @@ version_check_dnsmasq() { # If a file cannot be found, printf " %b No dnsmasq.conf found... restoring default dnsmasq.conf..." "${INFO}" # restore the default one - cp ${dnsmasq_original_config} ${dnsmasq_conf} + 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 Copying 01-pihole.conf to /etc/dnsmasq.d/01-pihole.conf..." "${INFO}" # 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 - mkdir "/etc/dnsmasq.d" + install -d -m 755 "/etc/dnsmasq.d" fi # Copy the new Pi-hole DNS config file into the dnsmasq.d directory - cp ${dnsmasq_pihole_01_snippet} ${dnsmasq_pihole_01_location} + install -D -m 644 -T "${dnsmasq_pihole_01_snippet}" "${dnsmasq_pihole_01_location}" printf "%b %b Copying 01-pihole.conf to /etc/dnsmasq.d/01-pihole.conf\\n" "${OVER}" "${TICK}" # 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_location} + sed -i "s/@INT@/$PIHOLE_INTERFACE/" "${dnsmasq_pihole_01_location}" if [[ "${PIHOLE_DNS_1}" != "" ]]; then # Then swap in the primary DNS server - sed -i "s/@DNS1@/$PIHOLE_DNS_1/" ${dnsmasq_pihole_01_location} + sed -i "s/@DNS1@/$PIHOLE_DNS_1/" "${dnsmasq_pihole_01_location}" else # - sed -i '/^server=@DNS1@/d' ${dnsmasq_pihole_01_location} + sed -i '/^server=@DNS1@/d' "${dnsmasq_pihole_01_location}" fi if [[ "${PIHOLE_DNS_2}" != "" ]]; then # Then swap in the primary DNS server - sed -i "s/@DNS2@/$PIHOLE_DNS_2/" ${dnsmasq_pihole_01_location} + sed -i "s/@DNS2@/$PIHOLE_DNS_2/" "${dnsmasq_pihole_01_location}" else # - sed -i '/^server=@DNS2@/d' ${dnsmasq_pihole_01_location} + sed -i '/^server=@DNS2@/d' "${dnsmasq_pihole_01_location}" fi # - sed -i 's/^#conf-dir=\/etc\/dnsmasq.d$/conf-dir=\/etc\/dnsmasq.d/' ${dnsmasq_conf} + 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_location} + sed -i 's/^log-queries/#log-queries/' "${dnsmasq_pihole_01_location}" # Otherwise, else # enable it by uncommenting the directive in the DNS config file - sed -i 's/^#log-queries/log-queries/' ${dnsmasq_pihole_01_location} + sed -i 's/^#log-queries/log-queries/' "${dnsmasq_pihole_01_location}" fi } @@ -1360,6 +1398,7 @@ installConfigs() { # 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 empty file if it does not exist if [[ ! -r "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" ]]; then @@ -1369,28 +1408,22 @@ installConfigs() { return 1 fi fi - # Install an empty regex file - if [[ ! -f "${regexFile}" ]]; then - # Let PHP edit the regex file, if installed - install -o pihole -g "${LIGHTTPD_GROUP:-pihole}" -m 664 /dev/null "${regexFile}" - fi # 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 - mkdir /etc/lighttpd - # and set the owners - chown "${USER}":root /etc/lighttpd + # 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 "/etc/lighttpd/lighttpd.conf" ]]; then # back up the original mv /etc/lighttpd/lighttpd.conf /etc/lighttpd/lighttpd.conf.orig fi # and copy in the config file Pi-hole needs - cp ${PI_HOLE_LOCAL_REPO}/advanced/${LIGHTTPD_CFG} /etc/lighttpd/lighttpd.conf + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/${LIGHTTPD_CFG} /etc/lighttpd/lighttpd.conf # Make sure the external.conf file exists, as lighttpd v1.4.50 crashes without it touch /etc/lighttpd/external.conf + chmod 644 /etc/lighttpd/external.conf # if there is a custom block page in the html/pihole directory, replace 404 handler in lighttpd config if [[ -f "${PI_HOLE_BLOCKPAGE_DIR}/custom.php" ]]; then sed -i 's/^\(server\.error-handler-404\s*=\s*\).*$/\1"pihole\/custom\.php"/' /etc/lighttpd/lighttpd.conf @@ -1421,16 +1454,16 @@ install_manpage() { fi if [[ ! -d "/usr/local/share/man/man8" ]]; then # if not present, create man8 directory - mkdir /usr/local/share/man/man8 + install -d -m 755 /usr/local/share/man/man8 fi if [[ ! -d "/usr/local/share/man/man5" ]]; then - # if not present, create man8 directory - mkdir /usr/local/share/man/man5 + # 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 - cp ${PI_HOLE_LOCAL_REPO}/manpages/pihole.8 /usr/local/share/man/man8/pihole.8 - cp ${PI_HOLE_LOCAL_REPO}/manpages/pihole-FTL.8 /usr/local/share/man/man8/pihole-FTL.8 - cp ${PI_HOLE_LOCAL_REPO}/manpages/pihole-FTL.conf.5 /usr/local/share/man/man5/pihole-FTL.conf.5 + 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 + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/manpages/pihole-FTL.conf.5 /usr/local/share/man/man5/pihole-FTL.conf.5 if mandb -q &>/dev/null; then # Updated successfully printf "%b %b man pages installed and database updated\\n" "${OVER}" "${TICK}" @@ -1612,20 +1645,23 @@ install_dependent_packages() { # 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 debconf-apt-progress ; then + if is_command apt-get ; then # For each package, 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 - echo -e "${OVER} ${INFO} Checking for $i (will be installed)" + printf "%b %b Checking for %s (will be installed)\\n" "${OVER}" "${INFO}" "${i}" installArray+=("${i}") fi done if [[ "${#installArray[@]}" -gt 0 ]]; then test_dpkg_lock - debconf-apt-progress -- "${PKG_INSTALL[@]}" "${installArray[@]}" + printf " %b Processing %s install(s) for: %s, please wait...\\n" "${INFO}" "${PKG_MANAGER}" "${installArray[*]}" + printf '%*s\n' "$columns" '' | tr " " -; + "${PKG_INSTALL[@]}" "${installArray[@]}" + printf '%*s\n' "$columns" '' | tr " " -; return fi printf "\\n" @@ -1635,15 +1671,18 @@ install_dependent_packages() { # Install Fedora/CentOS packages for i in "$@"; do printf " %b Checking for %s..." "${INFO}" "${i}" - if ${PKG_MANAGER} -q list installed "${i}" &> /dev/null; then - printf "%b %b Checking for %s" "${OVER}" "${TICK}" "${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)" "${OVER}" "${INFO}" "${i}" + printf "%b %b Checking for %s (will be installed)\\n" "${OVER}" "${INFO}" "${i}" installArray+=("${i}") fi done if [[ "${#installArray[@]}" -gt 0 ]]; then - "${PKG_INSTALL[@]}" "${installArray[@]}" &> /dev/null + printf " %b Processing %s install(s) for: %s, please wait...\\n" "${INFO}" "${PKG_MANAGER}" "${installArray[*]}" + printf '%*s\n' "$columns" '' | tr " " -; + "${PKG_INSTALL[@]}" "${installArray[@]}" + printf '%*s\n' "$columns" '' | tr " " -; return fi printf "\\n" @@ -1659,7 +1698,7 @@ installPiholeWeb() { # Install the directory install -d -m 0755 ${PI_HOLE_BLOCKPAGE_DIR} # and the blockpage - install -D ${PI_HOLE_LOCAL_REPO}/advanced/{index,blockingpage}.* ${PI_HOLE_BLOCKPAGE_DIR}/ + install -D -m 644 ${PI_HOLE_LOCAL_REPO}/advanced/{index,blockingpage}.* ${PI_HOLE_BLOCKPAGE_DIR}/ # Remove superseded file if [[ -e "${PI_HOLE_BLOCKPAGE_DIR}/index.js" ]]; then @@ -1678,7 +1717,7 @@ installPiholeWeb() { # Otherwise, else # don't do anything - printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" + printf "%b %b %s\\n" "${OVER}" "${INFO}" "${str}" printf " No default index.lighttpd.html file found... not backing up\\n" fi @@ -1686,7 +1725,7 @@ installPiholeWeb() { local str="Installing sudoer file" printf "\\n %b %s..." "${INFO}" "${str}" # Make the .d directory if it doesn't exist - mkdir -p /etc/sudoers.d/ + 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 @@ -1709,7 +1748,8 @@ installCron() { local str="Installing latest Cron script" printf "\\n %b %s..." "${INFO}" "${str}" # Copy the cron file over from the local repo - cp ${PI_HOLE_LOCAL_REPO}/advanced/Templates/pihole.cron /etc/cron.d/pihole + # 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 @@ -1752,7 +1792,7 @@ configureFirewall() { # If a firewall is running, if firewall-cmd --state &> /dev/null; then # ask if the user wants to install Pi-hole's default firewall rules - whiptail --title "Firewall in use" --yesno "We have detected a running firewall\\n\\nPi-hole currently requires HTTP and DNS port access.\\n\\n\\n\\nInstall Pi-hole default firewall rules?" ${r} ${c} || \ + whiptail --title "Firewall in use" --yesno "We have detected a running firewall\\n\\nPi-hole currently requires HTTP and DNS port access.\\n\\n\\n\\nInstall Pi-hole default firewall rules?" "${r}" "${c}" || \ { printf " %b Not installing firewall rulesets.\\n" "${INFO}"; return 0; } printf " %b Configuring FirewallD for httpd and pihole-FTL\\n" "${TICK}" # Allow HTTP and DNS traffic @@ -1765,7 +1805,7 @@ configureFirewall() { # If chain Policy is not ACCEPT or last Rule is not ACCEPT # then check and insert our Rules above the DROP/REJECT Rule. if iptables -S INPUT | head -n1 | grep -qv '^-P.*ACCEPT$' || iptables -S INPUT | tail -n1 | grep -qv '^-\(A\|P\).*ACCEPT$'; then - whiptail --title "Firewall in use" --yesno "We have detected a running firewall\\n\\nPi-hole currently requires HTTP and DNS port access.\\n\\n\\n\\nInstall Pi-hole default firewall rules?" ${r} ${c} || \ + whiptail --title "Firewall in use" --yesno "We have detected a running firewall\\n\\nPi-hole currently requires HTTP and DNS port access.\\n\\n\\n\\nInstall Pi-hole default firewall rules?" "${r}" "${c}" || \ { printf " %b Not installing firewall rulesets.\\n" "${INFO}"; return 0; } printf " %b Installing new IPTables firewall rulesets\\n" "${TICK}" # Check chain first, otherwise a new rule will duplicate old ones @@ -1817,6 +1857,7 @@ finalExports() { echo "INSTALL_WEB_INTERFACE=${INSTALL_WEB_INTERFACE}" echo "LIGHTTPD_ENABLED=${LIGHTTPD_ENABLED}" }>> "${setupVars}" + chmod 644 "${setupVars}" # Set the privacy level sed -i '/PRIVACYLEVEL/d' "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" @@ -1839,7 +1880,7 @@ installLogrotate() { local str="Installing latest logrotate script" printf "\\n %b %s..." "${INFO}" "${str}" # Copy the file over from the local repo - cp ${PI_HOLE_LOCAL_REPO}/advanced/Templates/logrotate /etc/pihole/logrotate + install -D -m 644 -T ${PI_HOLE_LOCAL_REPO}/advanced/Templates/logrotate /etc/pihole/logrotate # Different operating systems have different user / group # settings for logrotate that makes it impossible to create # a static logrotate file that will work with e.g. @@ -1858,29 +1899,26 @@ installLogrotate() { # At some point in the future this list can be pruned, for now we'll need it to ensure updates don't break. # Refactoring of install script has changed the name of a couple of variables. Sort them out here. accountForRefactor() { - sed -i 's/piholeInterface/PIHOLE_INTERFACE/g' ${setupVars} - sed -i 's/IPv4_address/IPV4_ADDRESS/g' ${setupVars} - sed -i 's/IPv4addr/IPV4_ADDRESS/g' ${setupVars} - sed -i 's/IPv6_address/IPV6_ADDRESS/g' ${setupVars} - sed -i 's/piholeIPv6/IPV6_ADDRESS/g' ${setupVars} - sed -i 's/piholeDNS1/PIHOLE_DNS_1/g' ${setupVars} - sed -i 's/piholeDNS2/PIHOLE_DNS_2/g' ${setupVars} - sed -i 's/^INSTALL_WEB=/INSTALL_WEB_INTERFACE=/' ${setupVars} + sed -i 's/piholeInterface/PIHOLE_INTERFACE/g' "${setupVars}" + sed -i 's/IPv4_address/IPV4_ADDRESS/g' "${setupVars}" + sed -i 's/IPv4addr/IPV4_ADDRESS/g' "${setupVars}" + sed -i 's/IPv6_address/IPV6_ADDRESS/g' "${setupVars}" + sed -i 's/piholeIPv6/IPV6_ADDRESS/g' "${setupVars}" + sed -i 's/piholeDNS1/PIHOLE_DNS_1/g' "${setupVars}" + sed -i 's/piholeDNS2/PIHOLE_DNS_2/g' "${setupVars}" + sed -i 's/^INSTALL_WEB=/INSTALL_WEB_INTERFACE=/' "${setupVars}" # Add 'INSTALL_WEB_SERVER', if its not been applied already: https://github.com/pi-hole/pi-hole/pull/2115 if ! grep -q '^INSTALL_WEB_SERVER=' ${setupVars}; then local webserver_installed=false if grep -q '^INSTALL_WEB_INTERFACE=true' ${setupVars}; then webserver_installed=true fi - echo -e "INSTALL_WEB_SERVER=$webserver_installed" >> ${setupVars} + echo -e "INSTALL_WEB_SERVER=$webserver_installed" >> "${setupVars}" fi } # Install base files and web interface installPihole() { - # Create the pihole user - create_pihole_user - # If the user wants to install the Web interface, if [[ "${INSTALL_WEB_INTERFACE}" == true ]]; then if [[ ! -d "${webroot}" ]]; then @@ -1892,8 +1930,14 @@ installPihole() { # Set the owner and permissions chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} ${webroot} chmod 0775 ${webroot} + # Repair permissions if /var/www/html is not world readable + chmod a+rx /var/www + chmod a+rx /var/www/html # Give pihole access to the Web server group usermod -a -G ${LIGHTTPD_GROUP} pihole + # 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 @@ -1945,20 +1989,42 @@ installPihole() { # SELinux checkSelinux() { - # If the getenforce command exists, - if is_command getenforce ; then - # Store the current mode in a variable - enforceMode=$(getenforce) - printf "\\n %b SELinux mode detected: %s\\n" "${INFO}" "${enforceMode}" - - # If it's enforcing, - if [[ "${enforceMode}" == "Enforcing" ]]; then - # Explain Pi-hole does not support it yet - whiptail --defaultno --title "SELinux Enforcing Detected" --yesno "SELinux is being ENFORCED on your system! \\n\\nPi-hole currently does not support SELinux, but you may still continue with the installation.\\n\\nNote: Web Admin will not be fully functional unless you set your policies correctly\\n\\nContinue installing Pi-hole?" ${r} ${c} || \ - { printf "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } - printf " %b Continuing installation with SELinux Enforcing\\n" "${INFO}" - printf " %b Please refer to official SELinux documentation to create a custom policy\\n" "${INFO}" - fi + local DEFAULT_SELINUX + local CURRENT_SELINUX + local SELINUX_ENFORCING=0 + # Check if a SELinux configuration file exists + if [[ -f /etc/selinux/config ]]; then + # If a SELinux configuration file was found, 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 "\\n %bSELinux Enforcing detected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; + exit 1; fi } @@ -1993,7 +2059,7 @@ If you set a new IP address, you should restart the Pi. The install log is in /etc/pihole. -${additional}" ${r} ${c} +${additional}" "${r}" "${c}" } update_dialogs() { @@ -2014,7 +2080,7 @@ update_dialogs() { opt2b="This will reset your Pi-hole and allow you to enter new settings." # Display the information to the user - UpdateCmd=$(whiptail --title "Existing Install Detected!" --menu "\\n\\nWe have detected an existing install.\\n\\nPlease choose from the following options: \\n($strAdd)" ${r} ${c} 2 \ + UpdateCmd=$(whiptail --title "Existing Install Detected!" --menu "\\n\\nWe have detected an existing install.\\n\\nPlease choose from the following options: \\n($strAdd)" "${r}" "${c}" 2 \ "${opt1a}" "${opt1b}" \ "${opt2a}" "${opt2b}" 3>&2 2>&1 1>&3) || \ { printf " %bCancel was selected, exiting installer%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } @@ -2103,6 +2169,8 @@ checkout_pull_branch() { 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 || return 1) @@ -2196,6 +2264,8 @@ FTLinstall() { # 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 + chmod 644 "${PI_HOLE_CONFIG_DIR}/macvendor.db" + chown pihole:pihole "${PI_HOLE_CONFIG_DIR}/macvendor.db" # Stop pihole-FTL service if available stop_service pihole-FTL &> /dev/null @@ -2246,6 +2316,7 @@ disable_dnsmasq() { fi # Create /etc/dnsmasq.conf echo "conf-dir=/etc/dnsmasq.d" > "${conffile}" + chmod 644 "${conffile}" } get_binary_name() { @@ -2285,9 +2356,15 @@ get_binary_name() { l_binary="pihole-FTL-arm-linux-gnueabi" fi else - printf "%b %b Detected ARM architecture\\n" "${OVER}" "${TICK}" - # set the binary to be used - l_binary="pihole-FTL-arm-linux-gnueabi" + if [[ -f "/.dockerenv" ]]; then + printf "%b %b Detected ARM architecture in docker\\n" "${OVER}" "${TICK}" + # set the binary to be used + binary="pihole-FTL-armel-native" + else + printf "%b %b Detected ARM architecture\\n" "${OVER}" "${TICK}" + # set the binary to be used + binary="pihole-FTL-arm-linux-gnueabi" + fi fi elif [[ "${machine}" == "x86_64" ]]; then # This gives the architecture of packages dpkg installs (for example, "i386") @@ -2439,6 +2516,7 @@ 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() { @@ -2523,7 +2601,7 @@ main() { # Display welcome dialogs welcomeDialogs # Create directory for Pi-hole storage - mkdir -p /etc/pihole/ + install -d -m 755 /etc/pihole/ # Determine available interfaces get_available_interfaces # Find interfaces and let the user choose one @@ -2545,7 +2623,7 @@ main() { installDefaultBlocklists # Source ${setupVars} to use predefined user variables in the functions - source ${setupVars} + source "${setupVars}" # Get the privacy level if it exists (default is 0) if [[ -f "${PI_HOLE_CONFIG_DIR}/pihole-FTL.conf" ]]; then @@ -2609,7 +2687,7 @@ main() { 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} + echo "WEBPASSWORD=$(HashPassword "${pw}")" >> "${setupVars}" fi fi diff --git a/automated install/uninstall.sh b/automated install/uninstall.sh index 5cd5b924..2d6837b4 100755 --- a/automated install/uninstall.sh +++ b/automated install/uninstall.sh @@ -156,7 +156,7 @@ removeNoPurge() { # Restore Resolved if [[ -e /etc/systemd/resolved.conf.orig ]]; then - ${SUDO} cp /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf + ${SUDO} cp -p /etc/systemd/resolved.conf.orig /etc/systemd/resolved.conf systemctl reload-or-restart systemd-resolved fi diff --git a/gravity.sh b/gravity.sh index cca6279d..c421e832 100755 --- a/gravity.sh +++ b/gravity.sh @@ -17,37 +17,34 @@ coltable="/opt/pihole/COL_TABLE" source "${coltable}" regexconverter="/opt/pihole/wildcard_regex_converter.sh" source "${regexconverter}" +# shellcheck disable=SC1091 +source "/etc/.pihole/advanced/Scripts/database_migration/gravity-db.sh" basename="pihole" PIHOLE_COMMAND="/usr/local/bin/${basename}" piholeDir="/etc/${basename}" -adListFile="${piholeDir}/adlists.list" -adListDefault="${piholeDir}/adlists.default" - +# Legacy (pre v5.0) list file locations whitelistFile="${piholeDir}/whitelist.txt" blacklistFile="${piholeDir}/blacklist.txt" regexFile="${piholeDir}/regex.list" +adListFile="${piholeDir}/adlists.list" -adList="${piholeDir}/gravity.list" -blackList="${piholeDir}/black.list" localList="${piholeDir}/local.list" VPNList="/etc/openvpn/ipp.txt" -domainsExtension="domains" -matterAndLight="${basename}.0.matterandlight.txt" -parsedMatter="${basename}.1.parsedmatter.txt" -whitelistMatter="${basename}.2.whitelistmatter.txt" -accretionDisc="${basename}.3.accretionDisc.txt" -preEventHorizon="list.preEventHorizon" +piholeGitDir="/etc/.pihole" +gravityDBfile="${piholeDir}/gravity.db" +gravityTEMPfile="${piholeDir}/gravity_temp.db" +gravityDBschema="${piholeGitDir}/advanced/Templates/gravity.db.sql" +gravityDBcopy="${piholeGitDir}/advanced/Templates/gravity_copy.sql" +optimize_database=false -skipDownload="false" +domainsExtension="domains" resolver="pihole-FTL" -haveSourceUrls=true - # Source setupVars from install script setupVars="${piholeDir}/setupVars.conf" if [[ -f "${setupVars}" ]];then @@ -83,31 +80,186 @@ if [[ -r "${piholeDir}/pihole.conf" ]]; then echo -e " ${COL_LIGHT_RED}Ignoring overrides specified within pihole.conf! ${COL_NC}" fi -# Determine if Pi-hole blocking is disabled -# If this is the case, we want to update -# gravity.list.bck and black.list.bck instead of -# gravity.list and black.list -detect_pihole_blocking_status() { - if [[ "${BLOCKING_ENABLED}" == false ]]; then - echo -e " ${INFO} Pi-hole blocking is disabled" - adList="${adList}.bck" - blackList="${blackList}.bck" - else - echo -e " ${INFO} Pi-hole blocking is enabled" +# Generate new sqlite3 file from schema template +generate_gravity_database() { + sqlite3 "${1}" < "${gravityDBschema}" +} + +# Copy data from old to new database file and swap them +gravity_swap_databases() { + local str + str="Building tree" + echo -ne " ${INFO} ${str}..." + + # The index is intentionally not UNIQUE as prro quality adlists may contain domains more than once + output=$( { sqlite3 "${gravityTEMPfile}" "CREATE INDEX idx_gravity ON gravity (domain, adlist_id);"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to build gravity tree in ${gravityTEMPfile}\\n ${output}" + return 1 fi + echo -e "${OVER} ${TICK} ${str}" + + str="Swapping databases" + echo -ne " ${INFO} ${str}..." + + output=$( { sqlite3 "${gravityTEMPfile}" < "${gravityDBcopy}"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to copy data from ${gravityDBfile} to ${gravityTEMPfile}\\n ${output}" + return 1 + fi + echo -e "${OVER} ${TICK} ${str}" + + # Swap databases and remove old database + rm "${gravityDBfile}" + mv "${gravityTEMPfile}" "${gravityDBfile}" +} + +# Update timestamp when the gravity table was last updated successfully +update_gravity_timestamp() { + output=$( { printf ".timeout 30000\\nINSERT OR REPLACE INTO info (property,value) values ('updated',cast(strftime('%%s', 'now') as int));" | sqlite3 "${gravityDBfile}"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to update gravity timestamp in database ${gravityDBfile}\\n ${output}" + return 1 + fi + return 0 +} + +# Import domains from file and store them in the specified database table +database_table_from_file() { + # Define locals + local table source backup_path backup_file tmpFile type + table="${1}" + source="${2}" + backup_path="${piholeDir}/migration_backup" + backup_file="${backup_path}/$(basename "${2}")" + tmpFile="$(mktemp -p "/tmp" --suffix=".gravity")" + + local timestamp + timestamp="$(date --utc +'%s')" + + local rowid + declare -i rowid + rowid=1 + + # Special handling for domains to be imported into the common domainlist table + if [[ "${table}" == "whitelist" ]]; then + type="0" + table="domainlist" + elif [[ "${table}" == "blacklist" ]]; then + type="1" + table="domainlist" + elif [[ "${table}" == "regex" ]]; then + type="3" + table="domainlist" + fi + + # Get MAX(id) from domainlist when INSERTing into this table + if [[ "${table}" == "domainlist" ]]; then + rowid="$(sqlite3 "${gravityDBfile}" "SELECT MAX(id) FROM domainlist;")" + if [[ -z "$rowid" ]]; then + rowid=0 + fi + rowid+=1 + fi + + # Loop over all domains in ${source} file + # Read file line by line + grep -v '^ *#' < "${source}" | while IFS= read -r domain + do + # Only add non-empty lines + if [[ -n "${domain}" ]]; then + if [[ "${table}" == "domain_audit" ]]; then + # domain_audit table format (no enable or modified fields) + echo "${rowid},\"${domain}\",${timestamp}" >> "${tmpFile}" + elif [[ "${table}" == "adlist" ]]; then + # Adlist table format + echo "${rowid},\"${domain}\",1,${timestamp},${timestamp},\"Migrated from ${source}\"" >> "${tmpFile}" + else + # White-, black-, and regexlist table format + echo "${rowid},${type},\"${domain}\",1,${timestamp},${timestamp},\"Migrated from ${source}\"" >> "${tmpFile}" + fi + rowid+=1 + fi + done + + # Store domains in database table specified by ${table} + # Use printf as .mode and .import need to be on separate lines + # see https://unix.stackexchange.com/a/445615/83260 + output=$( { printf ".timeout 30000\\n.mode csv\\n.import \"%s\" %s\\n" "${tmpFile}" "${table}" | sqlite3 "${gravityDBfile}"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to fill table ${table}${type} in database ${gravityDBfile}\\n ${output}" + gravity_Cleanup "error" + fi + + # Move source file to backup directory, create directory if not existing + mkdir -p "${backup_path}" + mv "${source}" "${backup_file}" 2> /dev/null || \ + echo -e " ${CROSS} Unable to backup ${source} to ${backup_path}" + + # Delete tmpFile + rm "${tmpFile}" > /dev/null 2>&1 || \ + echo -e " ${CROSS} Unable to remove ${tmpFile}" +} + +# Migrate pre-v5.0 list files to database-based Pi-hole versions +migrate_to_database() { + # Create database file only if not present + if [ ! -e "${gravityDBfile}" ]; then + # Create new database file - note that this will be created in version 1 + echo -e " ${INFO} Creating new gravity database" + generate_gravity_database "${gravityDBfile}" + + # Check if gravity database needs to be updated + upgrade_gravityDB "${gravityDBfile}" "${piholeDir}" + + # Migrate list files to new database + if [ -e "${adListFile}" ]; then + # Store adlist domains in database + echo -e " ${INFO} Migrating content of ${adListFile} into new database" + database_table_from_file "adlist" "${adListFile}" + fi + if [ -e "${blacklistFile}" ]; then + # Store blacklisted domains in database + echo -e " ${INFO} Migrating content of ${blacklistFile} into new database" + database_table_from_file "blacklist" "${blacklistFile}" + fi + if [ -e "${whitelistFile}" ]; then + # Store whitelisted domains in database + echo -e " ${INFO} Migrating content of ${whitelistFile} into new database" + database_table_from_file "whitelist" "${whitelistFile}" + fi + if [ -e "${regexFile}" ]; then + # Store regex domains in database + # Important note: We need to add the domains to the "regex" table + # as it will only later be renamed to "regex_blacklist"! + echo -e " ${INFO} Migrating content of ${regexFile} into new database" + database_table_from_file "regex" "${regexFile}" + fi + fi + + # Check if gravity database needs to be updated + upgrade_gravityDB "${gravityDBfile}" "${piholeDir}" } # Determine if DNS resolution is available before proceeding gravity_CheckDNSResolutionAvailable() { local lookupDomain="pi.hole" - # Determine if $localList does not exist - if [[ ! -e "${localList}" ]]; then + # Determine if $localList does not exist, and ensure it is not empty + if [[ ! -e "${localList}" ]] || [[ -s "${localList}" ]]; then lookupDomain="raw.githubusercontent.com" fi # Determine if $lookupDomain is resolvable - if timeout 1 getent hosts "${lookupDomain}" &> /dev/null; then + if timeout 4 getent hosts "${lookupDomain}" &> /dev/null; then # Print confirmation of resolvability if it had previously failed if [[ -n "${secs:-}" ]]; then echo -e "${OVER} ${TICK} DNS resolution is now available\\n" @@ -121,7 +273,7 @@ gravity_CheckDNSResolutionAvailable() { # If the /etc/resolv.conf contains resolvers other than 127.0.0.1 then the local dnsmasq will not be queried and pi.hole is NXDOMAIN. # This means that even though name resolution is working, the getent hosts check fails and the holddown timer keeps ticking and eventualy fails # So we check the output of the last command and if it failed, attempt to use dig +short as a fallback - if timeout 1 dig +short "${lookupDomain}" &> /dev/null; then + if timeout 4 dig +short "${lookupDomain}" &> /dev/null; then if [[ -n "${secs:-}" ]]; then echo -e "${OVER} ${TICK} DNS resolution is now available\\n" fi @@ -153,19 +305,14 @@ gravity_CheckDNSResolutionAvailable() { gravity_CheckDNSResolutionAvailable } -# Retrieve blocklist URLs and parse domains from adlists.list -gravity_GetBlocklistUrls() { +# Retrieve blocklist URLs and parse domains from adlist.list +gravity_DownloadBlocklists() { echo -e " ${INFO} ${COL_BOLD}Neutrino emissions detected${COL_NC}..." - if [[ -f "${adListDefault}" ]] && [[ -f "${adListFile}" ]]; then - # Remove superceded $adListDefault file - rm "${adListDefault}" 2> /dev/null || \ - echo -e " ${CROSS} Unable to remove ${adListDefault}" - fi - - # Retrieve source URLs from $adListFile - # Logic: Remove comments and empty lines - mapfile -t sources <<< "$(grep -v -E "^(#|$)" "${adListFile}" 2> /dev/null)" + # Retrieve source URLs from gravity database + # We source only enabled adlists, sqlite3 stores boolean values as 0 (false) or 1 (true) + mapfile -t sources <<< "$(sqlite3 "${gravityDBfile}" "SELECT address FROM vw_adlist;" 2> /dev/null)" + mapfile -t sourceIDs <<< "$(sqlite3 "${gravityDBfile}" "SELECT id FROM vw_adlist;" 2> /dev/null)" # Parse source domains from $sources mapfile -t sourceDomains <<< "$( @@ -186,16 +333,28 @@ gravity_GetBlocklistUrls() { echo -e "${OVER} ${CROSS} ${str}" echo -e " ${INFO} No source list found, or it is empty" echo "" - haveSourceUrls=false + return 1 fi -} - -# Define options for when retrieving blocklists -gravity_SetDownloadOptions() { - local url domain agent cmd_ext str + local url domain agent cmd_ext str target echo "" + # Prepare new gravity database + str="Preparing new gravity database" + echo -ne " ${INFO} ${str}..." + rm "${gravityTEMPfile}" > /dev/null 2>&1 + output=$( { sqlite3 "${gravityTEMPfile}" < "${gravityDBschema}"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to create new database ${gravityTEMPfile}\\n ${output}" + gravity_Cleanup "error" + else + echo -e "${OVER} ${TICK} ${str}" + fi + + target="$(mktemp -p "/tmp" --suffix=".gravity")" + # Loop through $sources and download each one for ((i = 0; i < "${#sources[@]}"; i++)); do url="${sources[$i]}" @@ -214,18 +373,83 @@ gravity_SetDownloadOptions() { *) cmd_ext="";; esac - if [[ "${skipDownload}" == false ]]; then - echo -e " ${INFO} Target: ${domain} (${url##*/})" - gravity_DownloadBlocklistFromUrl "${url}" "${cmd_ext}" "${agent}" - echo "" - fi + echo -e " ${INFO} Target: ${url}" + gravity_DownloadBlocklistFromUrl "${url}" "${cmd_ext}" "${agent}" "${sourceIDs[$i]}" "${saveLocation}" "${target}" + echo "" done + + str="Storing downloaded domains in new gravity database" + echo -ne " ${INFO} ${str}..." + output=$( { printf ".timeout 30000\\n.mode csv\\n.import \"%s\" gravity\\n" "${target}" | sqlite3 "${gravityTEMPfile}"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to fill gravity table in database ${gravityTEMPfile}\\n ${output}" + gravity_Cleanup "error" + else + echo -e "${OVER} ${TICK} ${str}" + fi + + if [[ "${status}" -eq 0 && -n "${output}" ]]; then + echo -e " Encountered non-critical SQL warnings. Please check the suitability of the lists you're using!\\n\\n SQL warnings:" + local warning file line lineno + while IFS= read -r line; do + echo " - ${line}" + warning="$(grep -oh "^[^:]*:[0-9]*" <<< "${line}")" + file="${warning%:*}" + lineno="${warning#*:}" + if [[ -n "${file}" && -n "${lineno}" ]]; then + echo -n " Line contains: " + awk "NR==${lineno}" < "${file}" + fi + done <<< "${output}" + echo "" + fi + + rm "${target}" > /dev/null 2>&1 || \ + echo -e " ${CROSS} Unable to remove ${target}" + gravity_Blackbody=true } +total_num=0 +parseList() { + local adlistID="${1}" src="${2}" target="${3}" incorrect_lines + # This sed does the following things: + # 1. Remove all domains containing invalid characters. Valid are: a-z, A-Z, 0-9, dot (.), minus (-), underscore (_) + # 2. Append ,adlistID to every line + # 3. Ensures there is a newline on the last line + sed -e "/[^a-zA-Z0-9.\_-]/d;s/$/,${adlistID}/;/.$/a\\" "${src}" >> "${target}" + # Find (up to) five domains containing invalid characters (see above) + incorrect_lines="$(sed -e "/[^a-zA-Z0-9.\_-]/!d" "${src}" | head -n 5)" + + local num_lines num_target_lines num_correct_lines num_invalid + # Get number of lines in source file + num_lines="$(grep -c "^" "${src}")" + # Get number of lines in destination file + num_target_lines="$(grep -c "^" "${target}")" + num_correct_lines="$(( num_target_lines-total_num ))" + total_num="$num_target_lines" + num_invalid="$(( num_lines-num_correct_lines ))" + if [[ "${num_invalid}" -eq 0 ]]; then + echo " ${INFO} Received ${num_lines} domains" + else + echo " ${INFO} Received ${num_lines} domains, ${num_invalid} domains invalid!" + fi + + # Display sample of invalid lines if we found some + if [[ -n "${incorrect_lines}" ]]; then + echo " Sample of invalid domains:" + while IFS= read -r line; do + echo " - ${line}" + done <<< "${incorrect_lines}" + fi +} + # Download specified URL and perform checks on HTTP status and file content gravity_DownloadBlocklistFromUrl() { - local url="${1}" cmd_ext="${2}" agent="${3}" heisenbergCompensator="" patternBuffer str httpCode success="" + local url="${1}" cmd_ext="${2}" agent="${3}" adlistID="${4}" saveLocation="${5}" target="${6}" + local heisenbergCompensator="" patternBuffer str httpCode success="" # Create temp file to store content on disk instead of RAM patternBuffer=$(mktemp -p "/tmp" --suffix=".phgpb") @@ -306,11 +530,14 @@ gravity_DownloadBlocklistFromUrl() { # Determine if the blocklist was downloaded and saved correctly if [[ "${success}" == true ]]; then if [[ "${httpCode}" == "304" ]]; then - : # Do not attempt to re-parse file + # Add domains to database table file + parseList "${adlistID}" "${saveLocation}" "${target}" # Check if $patternbuffer is a non-zero length file elif [[ -s "${patternBuffer}" ]]; then # Determine if blocklist is non-standard and parse as appropriate gravity_ParseFileIntoDomains "${patternBuffer}" "${saveLocation}" + # Add domains to database table file + parseList "${adlistID}" "${saveLocation}" "${target}" else # Fall back to previously cached list if $patternBuffer is empty echo -e " ${INFO} Received empty file: ${COL_LIGHT_GREEN}using previously cached list${COL_NC}" @@ -319,6 +546,8 @@ gravity_DownloadBlocklistFromUrl() { # Determine if cached list has read permission if [[ -r "${saveLocation}" ]]; then echo -e " ${CROSS} List download failed: ${COL_LIGHT_GREEN}using previously cached list${COL_NC}" + # Add domains to database table file + parseList "${adlistID}" "${saveLocation}" "${target}" else echo -e " ${CROSS} List download failed: ${COL_LIGHT_RED}no cached list available${COL_NC}" fi @@ -327,24 +556,27 @@ gravity_DownloadBlocklistFromUrl() { # Parse source files into domains format gravity_ParseFileIntoDomains() { - local source="${1}" destination="${2}" firstLine abpFilter + local source="${1}" destination="${2}" firstLine # Determine if we are parsing a consolidated list - if [[ "${source}" == "${piholeDir}/${matterAndLight}" ]]; then + #if [[ "${source}" == "${piholeDir}/${matterAndLight}" ]]; then # Remove comments and print only the domain name # Most of the lists downloaded are already in hosts file format but the spacing/formating is not contigious # This helps with that and makes it easier to read # It also helps with debugging so each stage of the script can be researched more in depth - # Awk -F splits on given IFS, we grab the right hand side (chops trailing #coments and /'s to grab the domain only. - # Last awk command takes non-commented lines and if they have 2 fields, take the right field (the domain) and leave - # the left (IP address), otherwise grab the single field. - - < ${source} awk -F '#' '{print $1}' | \ - awk -F '/' '{print $1}' | \ - awk '($1 !~ /^#/) { if (NF>1) {print $2} else {print $1}}' | \ - sed -nr -e 's/\.{2,}/./g' -e '/\./p' > ${destination} + # 1) Remove carriage returns + # 2) Convert all characters to lowercase + # 3) Remove lines containing "#" or "/" + # 4) Remove leading tabs, spaces, etc. + # 5) Delete lines not matching domain names + < "${source}" tr -d '\r' | \ + tr '[:upper:]' '[:lower:]' | \ + sed -r '/(\/|#).*$/d' | \ + sed -r 's/^.*\s+//g' | \ + sed -r '/([^\.]+\.)+[^\.]{2,}/!d' > "${destination}" + chmod 644 "${destination}" return 0 - fi + #fi # Individual file parsing: Keep comments, while parsing domains from each line # We keep comments to respect the list maintainer's licensing @@ -374,11 +606,13 @@ gravity_ParseFileIntoDomains() { # Print if nonempty length { print } ' "${source}" 2> /dev/null > "${destination}" + chmod 644 "${destination}" echo -e "${OVER} ${TICK} Format: URL" else # Default: Keep hosts/domains file in same format as it was downloaded output=$( { mv "${source}" "${destination}"; } 2>&1 ) + chmod 644 "${destination}" if [[ ! -e "${destination}" ]]; then echo -e "\\n ${CROSS} Unable to move tmp file to ${piholeDir} @@ -388,103 +622,29 @@ gravity_ParseFileIntoDomains() { fi } -# Create (unfiltered) "Matter and Light" consolidated list -gravity_ConsolidateDownloadedBlocklists() { - local str lastLine - - str="Consolidating blocklists" - if [[ "${haveSourceUrls}" == true ]]; then - echo -ne " ${INFO} ${str}..." +# Report number of entries in a table +gravity_Table_Count() { + local table="${1}" + local str="${2}" + local num + num="$(sqlite3 "${gravityDBfile}" "SELECT COUNT(*) FROM ${table};")" + if [[ "${table}" == "vw_gravity" ]]; then + local unique + unique="$(sqlite3 "${gravityDBfile}" "SELECT COUNT(DISTINCT domain) FROM ${table};")" + echo -e " ${INFO} Number of ${str}: ${num} (${unique} unique domains)" + sqlite3 "${gravityDBfile}" "INSERT OR REPLACE INTO info (property,value) VALUES ('gravity_count',${unique});" + else + echo -e " ${INFO} Number of ${str}: ${num}" fi - - # Empty $matterAndLight if it already exists, otherwise, create it - : > "${piholeDir}/${matterAndLight}" - - # Loop through each *.domains file - for i in "${activeDomains[@]}"; do - # Determine if file has read permissions, as download might have failed - if [[ -r "${i}" ]]; then - # Remove windows CRs from file, convert list to lower case, and append into $matterAndLight - tr -d '\r' < "${i}" | tr '[:upper:]' '[:lower:]' >> "${piholeDir}/${matterAndLight}" - - # Ensure that the first line of a new list is on a new line - lastLine=$(tail -1 "${piholeDir}/${matterAndLight}") - if [[ "${#lastLine}" -gt 0 ]]; then - echo "" >> "${piholeDir}/${matterAndLight}" - fi - fi - done - if [[ "${haveSourceUrls}" == true ]]; then - echo -e "${OVER} ${TICK} ${str}" - fi -} - -# Parse consolidated list into (filtered, unique) domains-only format -gravity_SortAndFilterConsolidatedList() { - local str num - - str="Extracting domains from blocklists" - if [[ "${haveSourceUrls}" == true ]]; then - echo -ne " ${INFO} ${str}..." - fi - - # Parse into hosts file - gravity_ParseFileIntoDomains "${piholeDir}/${matterAndLight}" "${piholeDir}/${parsedMatter}" - - # Format $parsedMatter line total as currency - num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${parsedMatter}")") - if [[ "${haveSourceUrls}" == true ]]; then - echo -e "${OVER} ${TICK} ${str}" - fi - echo -e " ${INFO} Number of domains being pulled in by gravity: ${COL_BLUE}${num}${COL_NC}" - - str="Removing duplicate domains" - if [[ "${haveSourceUrls}" == true ]]; then - echo -ne " ${INFO} ${str}..." - fi - - sort -u "${piholeDir}/${parsedMatter}" > "${piholeDir}/${preEventHorizon}" - - if [[ "${haveSourceUrls}" == true ]]; then - echo -e "${OVER} ${TICK} ${str}" - # Format $preEventHorizon line total as currency - num=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${preEventHorizon}")") - echo -e " ${INFO} Number of unique domains trapped in the Event Horizon: ${COL_BLUE}${num}${COL_NC}" - fi -} - -# Whitelist user-defined domains -gravity_Whitelist() { - local num str - - if [[ ! -f "${whitelistFile}" ]]; then - echo -e " ${INFO} Nothing to whitelist!" - return 0 - fi - - num=$(wc -l < "${whitelistFile}") - str="Number of whitelisted domains: ${num}" - echo -ne " ${INFO} ${str}..." - - # Print everything from preEventHorizon into whitelistMatter EXCEPT domains in $whitelistFile - comm -23 "${piholeDir}/${preEventHorizon}" <(sort "${whitelistFile}") > "${piholeDir}/${whitelistMatter}" - - echo -e "${OVER} ${INFO} ${str}" } # Output count of blacklisted domains and regex filters -gravity_ShowBlockCount() { - local num - - if [[ -f "${blacklistFile}" ]]; then - num=$(printf "%'.0f" "$(wc -l < "${blacklistFile}")") - echo -e " ${INFO} Number of blacklisted domains: ${num}" - fi - - if [[ -f "${regexFile}" ]]; then - num=$(grep -cv "^#" "${regexFile}") - echo -e " ${INFO} Number of regex filters: ${num}" - fi +gravity_ShowCount() { + gravity_Table_Count "vw_gravity" "gravity domains" "" + gravity_Table_Count "vw_blacklist" "exact blacklisted domains" + gravity_Table_Count "vw_regex_blacklist" "regex blacklist filters" + gravity_Table_Count "vw_whitelist" "exact whitelisted domains" + gravity_Table_Count "vw_regex_whitelist" "regex whitelist filters" } # Parse list of domains into hosts format @@ -504,7 +664,7 @@ gravity_ParseDomainsIntoHosts() { } # Create "localhost" entries into hosts format -gravity_ParseLocalDomains() { +gravity_generateLocalList() { local hostname if [[ -s "/etc/hostname" ]]; then @@ -520,6 +680,7 @@ gravity_ParseLocalDomains() { # Empty $localList if it already exists, otherwise, create it : > "${localList}" + chmod 644 "${localList}" gravity_ParseDomainsIntoHosts "${localList}.tmp" "${localList}" @@ -529,40 +690,6 @@ gravity_ParseLocalDomains() { fi } -# Create primary blacklist entries -gravity_ParseBlacklistDomains() { - local output status - - # Empty $accretionDisc if it already exists, otherwise, create it - : > "${piholeDir}/${accretionDisc}" - - if [[ -f "${piholeDir}/${whitelistMatter}" ]]; then - mv "${piholeDir}/${whitelistMatter}" "${piholeDir}/${accretionDisc}" - else - # There was no whitelist file, so use preEventHorizon instead of whitelistMatter. - cp "${piholeDir}/${preEventHorizon}" "${piholeDir}/${accretionDisc}" - fi - - # Move the file over as /etc/pihole/gravity.list so dnsmasq can use it - output=$( { mv "${piholeDir}/${accretionDisc}" "${adList}"; } 2>&1 ) - status="$?" - - if [[ "${status}" -ne 0 ]]; then - echo -e "\\n ${CROSS} Unable to move ${accretionDisc} from ${piholeDir}\\n ${output}" - gravity_Cleanup "error" - fi -} - -# Create user-added blacklist entries -gravity_ParseUserDomains() { - if [[ ! -f "${blacklistFile}" ]]; then - return 0 - fi - # Copy the file over as /etc/pihole/black.list so dnsmasq can use it - cp "${blacklistFile}" "${blackList}" 2> /dev/null || \ - echo -e "\\n ${CROSS} Unable to move ${blacklistFile##*/} to ${piholeDir}" -} - # Trap Ctrl-C gravity_Trap() { trap '{ echo -e "\\n\\n ${INFO} ${COL_LIGHT_RED}User-abort detected${COL_NC}"; gravity_Cleanup "error"; }' INT @@ -583,7 +710,7 @@ gravity_Cleanup() { # Ensure this function only runs when gravity_SetDownloadOptions() has completed if [[ "${gravity_Blackbody:-}" == true ]]; then # Remove any unused .domains files - for file in ${piholeDir}/*.${domainsExtension}; do + for file in "${piholeDir}"/*."${domainsExtension}"; do # If list is not in active array, then remove it if [[ ! "${activeDomains[*]}" == *"${file}"* ]]; then rm -f "${file}" 2> /dev/null || \ @@ -594,6 +721,21 @@ gravity_Cleanup() { echo -e "${OVER} ${TICK} ${str}" + if ${optimize_database} ; then + str="Optimizing domains database" + echo -ne " ${INFO} ${str}..." + # Run VACUUM command on database to optimize it + output=$( { sqlite3 "${gravityDBfile}" "VACUUM;"; } 2>&1 ) + status="$?" + + if [[ "${status}" -ne 0 ]]; then + echo -e "\\n ${CROSS} Unable to optimize gravity database ${gravityDBfile}\\n ${output}" + error="error" + else + echo -e "${OVER} ${TICK} ${str}" + fi + fi + # Only restart DNS service if offline if ! pidof ${resolver} &> /dev/null; then "${PIHOLE_COMMAND}" restartdns @@ -620,17 +762,28 @@ Options: for var in "$@"; do case "${var}" in "-f" | "--force" ) forceDelete=true;; + "-o" | "--optimize" ) optimize_database=true;; + "-r" | "--recreate" ) recreate_database=true;; "-h" | "--help" ) helpFunc;; - "-sd" | "--skip-download" ) skipDownload=true;; - "-b" | "--blacklist-only" ) listType="blacklist";; - "-w" | "--whitelist-only" ) listType="whitelist";; - "-wild" | "--wildcard-only" ) listType="wildcard"; dnsRestartType="restart";; esac done # Trap Ctrl-C gravity_Trap +if [[ "${recreate_database:-}" == true ]]; then + str="Restoring from migration backup" + echo -ne "${INFO} ${str}..." + rm "${gravityDBfile}" + pushd "${piholeDir}" > /dev/null || exit + cp migration_backup/* . + popd > /dev/null || exit + echo -e "${OVER} ${TICK} ${str}" +fi + +# Move possibly existing legacy files to the gravity database +migrate_to_database + if [[ "${forceDelete:-}" == true ]]; then str="Deleting existing list cache" echo -ne "${INFO} ${str}..." @@ -639,56 +792,32 @@ if [[ "${forceDelete:-}" == true ]]; then echo -e "${OVER} ${TICK} ${str}" fi -detect_pihole_blocking_status +# Gravity downloads blocklists next +gravity_CheckDNSResolutionAvailable +gravity_DownloadBlocklists -# Determine which functions to run -if [[ "${skipDownload}" == false ]]; then - # Gravity needs to download blocklists - gravity_CheckDNSResolutionAvailable - gravity_GetBlocklistUrls - if [[ "${haveSourceUrls}" == true ]]; then - gravity_SetDownloadOptions - fi - gravity_ConsolidateDownloadedBlocklists - gravity_SortAndFilterConsolidatedList -else - # Gravity needs to modify Blacklist/Whitelist/Wildcards - echo -e " ${INFO} Using cached Event Horizon list..." - numberOf=$(printf "%'.0f" "$(wc -l < "${piholeDir}/${preEventHorizon}")") - echo -e " ${INFO} ${COL_BLUE}${numberOf}${COL_NC} unique domains trapped in the Event Horizon" -fi +# Create local.list +gravity_generateLocalList -# Perform when downloading blocklists, or modifying the whitelist -if [[ "${skipDownload}" == false ]] || [[ "${listType}" == "whitelist" ]]; then - gravity_Whitelist -fi +# Migrate rest of the data from old to new database +gravity_swap_databases -convert_wildcard_to_regex -gravity_ShowBlockCount +# Update gravity timestamp +update_gravity_timestamp -# Perform when downloading blocklists, or modifying the white/blacklist (not wildcards) -if [[ "${skipDownload}" == false ]] || [[ "${listType}" == *"list" ]]; then - str="Parsing domains into hosts format" - echo -ne " ${INFO} ${str}..." +# Ensure proper permissions are set for the database +chown pihole:pihole "${gravityDBfile}" +chmod g+w "${piholeDir}" "${gravityDBfile}" - gravity_ParseUserDomains - - # Perform when downloading blocklists - if [[ ! "${listType:-}" == "blacklist" ]]; then - gravity_ParseLocalDomains - gravity_ParseBlacklistDomains - fi - - echo -e "${OVER} ${TICK} ${str}" - - gravity_Cleanup -fi - -echo "" +# Compute numbers to be displayed +gravity_ShowCount # Determine if DNS has been restarted by this instance of gravity if [[ -z "${dnsWasOffline:-}" ]]; then - # Use "force-reload" when restarting dnsmasq for everything but Wildcards - "${PIHOLE_COMMAND}" restartdns "${dnsRestartType:-force-reload}" + "${PIHOLE_COMMAND}" restartdns reload fi + +gravity_Cleanup +echo "" + "${PIHOLE_COMMAND}" status diff --git a/manpages/pihole.8 b/manpages/pihole.8 index b3008563..ed012092 100644 --- a/manpages/pihole.8 +++ b/manpages/pihole.8 @@ -66,14 +66,24 @@ Available commands and options: Adds or removes specified domain or domains to the blacklist .br +\fB--regex, regex\fR [options] [ ] +.br + Add or removes specified regex filter to the regex blacklist +.br + +\fB--white-regex\fR [options] [ ] +.br + Add or removes specified regex filter to the regex whitelist +.br + \fB--wild, wildcard\fR [options] [ ] .br Add or removes specified domain to the wildcard blacklist .br -\fB--regex, regex\fR [options] [ ] +\fB--white-wild\fR [options] [ ] .br - Add or removes specified regex filter to the regex blacklist + Add or removes specified domain to the wildcard whitelist .br (Whitelist/Blacklist manipulation options): @@ -351,6 +361,12 @@ Switching Pi-hole subsystem branches .br Switch to core development branch .br + +\fBpihole arpflush\fR +.br + Flush information stored in Pi-hole's network tables +.br + .SH "SEE ALSO" \fBlighttpd\fR(8), \fBpihole-FTL\fR(8) diff --git a/pihole b/pihole index d2ff3645..6e72b4a3 100755 --- a/pihole +++ b/pihole @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) @@ -10,11 +10,9 @@ # Please see LICENSE file for your rights under this license. readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" -readonly gravitylist="/etc/pihole/gravity.list" -readonly blacklist="/etc/pihole/black.list" -# setupVars and PI_HOLE_BIN_DIR are not readonly here because in some funcitons (checkout), -# it might get set again when the installer is sourced. This causes an +# setupVars and PI_HOLE_BIN_DIR are not readonly here because in some functions (checkout), +# they might get set again when the installer is sourced. This causes an # error due to modifying a readonly variable. setupVars="/etc/pihole/setupVars.conf" PI_HOLE_BIN_DIR="/usr/local/bin" @@ -57,6 +55,11 @@ flushFunc() { exit 0 } +arpFunc() { + "${PI_HOLE_SCRIPT_DIR}"/piholeARPTable.sh "$@" + exit 0 +} + updatePiholeFunc() { shift "${PI_HOLE_SCRIPT_DIR}"/update.sh "$@" @@ -102,17 +105,25 @@ restartDNS() { svcOption="${1:-restart}" # Determine if we should reload or restart - if [[ "${svcOption}" =~ "reload" ]]; then - # Reload has been requested + if [[ "${svcOption}" =~ "reload-lists" ]]; then + # Reloading of the lists has been requested + # Note: This will NOT re-read any *.conf files + # Note 2: We cannot use killall here as it does + # not know about real-time signals + svc="kill -SIGRTMIN $(pidof ${resolver})" + str="Reloading DNS lists" + elif [[ "${svcOption}" =~ "reload" ]]; then + # Reloading of the DNS cache has been requested # Note: This will NOT re-read any *.conf files svc="killall -s SIGHUP ${resolver}" + str="Flushing DNS cache" else # A full restart has been requested svc="service ${resolver} restart" + str="Restarting DNS server" fi # Print output to Terminal, but not to Web Admin - str="${svcOption^}ing DNS service" [[ -t 1 ]] && echo -ne " ${INFO} ${str}..." output=$( { ${svc}; } 2>&1 ) @@ -145,14 +156,6 @@ Time: echo -e " ${INFO} Blocking already disabled, nothing to do" exit 0 fi - if [[ -e "${gravitylist}" ]]; then - mv "${gravitylist}" "${gravitylist}.bck" - echo "" > "${gravitylist}" - fi - if [[ -e "${blacklist}" ]]; then - mv "${blacklist}" "${blacklist}.bck" - echo "" > "${blacklist}" - fi if [[ $# > 1 ]]; then local error=false if [[ "${2}" == *"s" ]]; then @@ -201,12 +204,6 @@ Time: echo -e " ${INFO} Enabling blocking" local str="Pi-hole Enabled" - if [[ -e "${gravitylist}.bck" ]]; then - mv "${gravitylist}.bck" "${gravitylist}" - fi - if [[ -e "${blacklist}.bck" ]]; then - mv "${blacklist}.bck" "${blacklist}" - fi sed -i "/BLOCKING_ENABLED=/d" "${setupVars}" echo "BLOCKING_ENABLED=true" >> "${setupVars}" fi @@ -309,8 +306,8 @@ tailFunc() { # Colour A/AAAA/DHCP strings as white # Colour everything else as gray tail -f /var/log/pihole.log | sed -E \ - -e "s,($(date +'%b %d ')| dnsmasq[.*[0-9]]),,g" \ - -e "s,(.*(gravity.list|black.list|regex.list| config ).* is (0.0.0.0|::|NXDOMAIN|${IPV4_ADDRESS%/*}|${IPV6_ADDRESS:-NULL}).*),${COL_RED}&${COL_NC}," \ + -e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \ + -e "s,(.*(blacklisted |gravity blocked ).* is (0.0.0.0|::|NXDOMAIN|${IPV4_ADDRESS%/*}|${IPV6_ADDRESS:-NULL}).*),${COL_RED}&${COL_NC}," \ -e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \ -e "s,.*,${COL_GRAY}&${COL_NC}," exit 0 @@ -383,8 +380,10 @@ Add '-h' after specific commands for more information on usage Whitelist/Blacklist Options: -w, whitelist Whitelist domain(s) -b, blacklist Blacklist domain(s) - --wild, wildcard Wildcard blacklist domain(s) - --regex, regex Regex blacklist domains(s) + --regex, regex Regex blacklist domains(s) + --white-regex Regex whitelist domains(s) + --wild, wildcard Wildcard blacklist domain(s) + --white-wild Wildcard whitelist domain(s) Add '-h' for more info on whitelist/blacklist usage Debugging Options: @@ -416,7 +415,8 @@ Options: Add '-h' for more info on disable usage restartdns Restart Pi-hole subsystems checkout Switch Pi-hole subsystems to a different Github branch - Add '-h' for more info on checkout usage"; + Add '-h' for more info on checkout usage + arpflush Flush information stored in Pi-hole's network tables"; exit 0 } @@ -443,8 +443,10 @@ fi case "${1}" in "-w" | "whitelist" ) listFunc "$@";; "-b" | "blacklist" ) listFunc "$@";; - "--wild" | "wildcard" ) listFunc "$@";; - "--regex" | "regex" ) listFunc "$@";; + "--wild" | "wildcard" ) listFunc "$@";; + "--regex" | "regex" ) listFunc "$@";; + "--white-regex" | "white-regex" ) listFunc "$@";; + "--white-wild" | "white-wild" ) listFunc "$@";; "-d" | "debug" ) debugFunc "$@";; "-f" | "flush" ) flushFunc "$@";; "-up" | "updatePihole" ) updatePiholeFunc "$@";; @@ -465,5 +467,6 @@ case "${1}" in "checkout" ) piholeCheckoutFunc "$@";; "tricorder" ) tricorderFunc;; "updatechecker" ) updateCheckFunc "$@";; + "arpflush" ) arpFunc "$@";; * ) helpFunc;; esac diff --git a/test/test_automated_install.py b/test/test_automated_install.py index 0c304c40..567ea241 100644 --- a/test/test_automated_install.py +++ b/test/test_automated_install.py @@ -254,73 +254,16 @@ def test_configureFirewall_IPTables_enabled_not_exist_no_errors(Pihole): assert len(re.findall(r'tcp --dport 4711:4720', firewall_calls)) == 2 -def test_selinux_enforcing_default_exit(Pihole): +def test_selinux_not_detected(Pihole): ''' - confirms installer prompts to exit when SELinux is Enforcing by default + confirms installer continues when SELinux configuration file does not exist ''' - # getenforce returns the running state of SELinux - mock_command('getenforce', {'*': ('Enforcing', '0')}, Pihole) - # Whiptail dialog returns Cancel for user prompt - mock_command('whiptail', {'*': ('', '1')}, Pihole) check_selinux = Pihole.run(''' + rm -f /etc/selinux/config source /opt/pihole/basic-install.sh checkSelinux ''') - expected_stdout = info_box + ' SELinux mode detected: Enforcing' - assert expected_stdout in check_selinux.stdout - expected_stdout = 'SELinux Enforcing detected, exiting installer' - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 1 - - -def test_selinux_enforcing_continue(Pihole): - ''' - confirms installer prompts to continue with custom policy warning - ''' - # getenforce returns the running state of SELinux - mock_command('getenforce', {'*': ('Enforcing', '0')}, Pihole) - # Whiptail dialog returns Continue for user prompt - mock_command('whiptail', {'*': ('', '0')}, Pihole) - check_selinux = Pihole.run(''' - source /opt/pihole/basic-install.sh - checkSelinux - ''') - expected_stdout = info_box + ' SELinux mode detected: Enforcing' - assert expected_stdout in check_selinux.stdout - expected_stdout = info_box + (' Continuing installation with SELinux ' - 'Enforcing') - assert expected_stdout in check_selinux.stdout - expected_stdout = info_box + (' Please refer to official SELinux ' - 'documentation to create a custom policy') - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 0 - - -def test_selinux_permissive(Pihole): - ''' - confirms installer continues when SELinux is Permissive - ''' - # getenforce returns the running state of SELinux - mock_command('getenforce', {'*': ('Permissive', '0')}, Pihole) - check_selinux = Pihole.run(''' - source /opt/pihole/basic-install.sh - checkSelinux - ''') - expected_stdout = info_box + ' SELinux mode detected: Permissive' - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 0 - - -def test_selinux_disabled(Pihole): - ''' - confirms installer continues when SELinux is Disabled - ''' - mock_command('getenforce', {'*': ('Disabled', '0')}, Pihole) - check_selinux = Pihole.run(''' - source /opt/pihole/basic-install.sh - checkSelinux - ''') - expected_stdout = info_box + ' SELinux mode detected: Disabled' + expected_stdout = info_box + ' SELinux not detected' assert expected_stdout in check_selinux.stdout assert check_selinux.rc == 0 @@ -338,7 +281,7 @@ def test_installPiholeWeb_fresh_install_no_errors(Pihole): expected_stdout = tick_box + (' Creating directory for blocking page, ' 'and copying files') assert expected_stdout in installWeb.stdout - expected_stdout = cross_box + ' Backing up index.lighttpd.html' + expected_stdout = info_box + ' Backing up index.lighttpd.html' assert expected_stdout in installWeb.stdout expected_stdout = ('No default index.lighttpd.html file found... ' 'not backing up') @@ -668,3 +611,42 @@ def test_IPv6_ULA_GUA_test(Pihole): ''') expected_stdout = 'Found IPv6 ULA address, using it for blocking IPv6 ads' assert expected_stdout in detectPlatform.stdout + + +def test_validate_ip_valid(Pihole): + ''' + Given a valid IP address, valid_ip returns success + ''' + + output = Pihole.run(''' + source /opt/pihole/basic-install.sh + valid_ip "192.168.1.1" + ''') + + assert output.rc == 0 + + +def test_validate_ip_invalid_octet(Pihole): + ''' + Given an invalid IP address (large octet), valid_ip returns an error + ''' + + output = Pihole.run(''' + source /opt/pihole/basic-install.sh + valid_ip "1092.168.1.1" + ''') + + assert output.rc == 1 + + +def test_validate_ip_invalid_letters(Pihole): + ''' + Given an invalid IP address (contains letters), valid_ip returns an error + ''' + + output = Pihole.run(''' + source /opt/pihole/basic-install.sh + valid_ip "not an IP" + ''') + + assert output.rc == 1 diff --git a/test/test_centos_fedora_support.py b/test/test_centos_fedora_support.py index df53d73f..aee16212 100644 --- a/test/test_centos_fedora_support.py +++ b/test/test_centos_fedora_support.py @@ -8,6 +8,69 @@ from conftest import ( ) +def mock_selinux_config(state, Pihole): + ''' + Creates a mock SELinux config file with expected content + ''' + # validate state string + valid_states = ['enforcing', 'permissive', 'disabled'] + assert state in valid_states + # getenforce returns the running state of SELinux + mock_command('getenforce', {'*': (state.capitalize(), '0')}, Pihole) + # create mock configuration with desired content + Pihole.run(''' + mkdir /etc/selinux + echo "SELINUX={state}" > /etc/selinux/config + '''.format(state=state.lower())) + + +@pytest.mark.parametrize("tag", [('centos'), ('fedora'), ]) +def test_selinux_enforcing_exit(Pihole): + ''' + confirms installer prompts to exit when SELinux is Enforcing by default + ''' + mock_selinux_config("enforcing", Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = cross_box + ' Current SELinux: Enforcing' + assert expected_stdout in check_selinux.stdout + expected_stdout = 'SELinux Enforcing detected, exiting installer' + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 1 + + +@pytest.mark.parametrize("tag", [('centos'), ('fedora'), ]) +def test_selinux_permissive(Pihole): + ''' + confirms installer continues when SELinux is Permissive + ''' + mock_selinux_config("permissive", Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = tick_box + ' Current SELinux: Permissive' + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 0 + + +@pytest.mark.parametrize("tag", [('centos'), ('fedora'), ]) +def test_selinux_disabled(Pihole): + ''' + confirms installer continues when SELinux is Disabled + ''' + mock_selinux_config("disabled", Pihole) + check_selinux = Pihole.run(''' + source /opt/pihole/basic-install.sh + checkSelinux + ''') + expected_stdout = tick_box + ' Current SELinux: Disabled' + assert expected_stdout in check_selinux.stdout + assert check_selinux.rc == 0 + + @pytest.mark.parametrize("tag", [('fedora'), ]) def test_epel_and_remi_not_installed_fedora(Pihole): '''