mirror of
https://github.com/pi-hole/pi-hole.git
synced 2024-11-22 14:13:42 +00:00
Finish core v6 implementation (#5689)
This commit is contained in:
commit
7b980ed9ac
9 changed files with 418 additions and 454 deletions
|
@ -21,7 +21,7 @@
|
||||||
TestAPIAvailability() {
|
TestAPIAvailability() {
|
||||||
|
|
||||||
# as we are running locally, we can get the port value from FTL directly
|
# as we are running locally, we can get the port value from FTL directly
|
||||||
local chaos_api_list availabilityResonse
|
local chaos_api_list availabilityResponse
|
||||||
|
|
||||||
# Query the API URLs from FTL using CHAOS TXT local.api.ftl
|
# Query the API URLs from FTL using CHAOS TXT local.api.ftl
|
||||||
# The result is a space-separated enumeration of full URLs
|
# The result is a space-separated enumeration of full URLs
|
||||||
|
@ -43,14 +43,20 @@ TestAPIAvailability() {
|
||||||
API_URL="${API_URL#\"}"
|
API_URL="${API_URL#\"}"
|
||||||
|
|
||||||
# Test if the API is available at this URL
|
# Test if the API is available at this URL
|
||||||
availabilityResonse=$(curl -skS -o /dev/null -w "%{http_code}" "${API_URL}auth")
|
availabilityResponse=$(curl -skS -o /dev/null -w "%{http_code}" "${API_URL}auth")
|
||||||
|
|
||||||
# Test if http status code was 200 (OK) or 401 (authentication required)
|
# Test if http status code was 200 (OK) or 401 (authentication required)
|
||||||
if [ ! "${availabilityResonse}" = 200 ] && [ ! "${availabilityResonse}" = 401 ]; then
|
if [ ! "${availabilityResponse}" = 200 ] && [ ! "${availabilityResponse}" = 401 ]; then
|
||||||
# API is not available at this port/protocol combination
|
# API is not available at this port/protocol combination
|
||||||
API_PORT=""
|
API_PORT=""
|
||||||
else
|
else
|
||||||
# API is available at this URL combination
|
# API is available at this URL combination
|
||||||
|
|
||||||
|
if [ "${availabilityResponse}" = 200 ]; then
|
||||||
|
# API is available without authentication
|
||||||
|
needAuth=false
|
||||||
|
fi
|
||||||
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -74,10 +80,28 @@ TestAPIAvailability() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
Authentication() {
|
LoginAPI() {
|
||||||
# Try to authenticate
|
# If the API URL is not set, test the availability
|
||||||
LoginAPI
|
if [ -z "${API_URL}" ]; then
|
||||||
|
TestAPIAvailability
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit early if authentication is not needed
|
||||||
|
if [ "${needAuth}" = false ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to read the CLI password (if enabled and readable by the current user)
|
||||||
|
if [ -r /etc/pihole/cli_pw ]; then
|
||||||
|
password=$(cat /etc/pihole/cli_pw)
|
||||||
|
|
||||||
|
# Try to authenticate using the CLI password
|
||||||
|
Authentication
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# If this did not work, ask the user for the password
|
||||||
while [ "${validSession}" = false ] || [ -z "${validSession}" ] ; do
|
while [ "${validSession}" = false ] || [ -z "${validSession}" ] ; do
|
||||||
echo "Authentication failed. Please enter your Pi-hole password"
|
echo "Authentication failed. Please enter your Pi-hole password"
|
||||||
|
|
||||||
|
@ -85,15 +109,12 @@ Authentication() {
|
||||||
secretRead; printf '\n'
|
secretRead; printf '\n'
|
||||||
|
|
||||||
# Try to authenticate again
|
# Try to authenticate again
|
||||||
LoginAPI
|
Authentication
|
||||||
done
|
done
|
||||||
|
|
||||||
# Loop exited, authentication was successful
|
|
||||||
echo "Authentication successful."
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginAPI() {
|
Authentication() {
|
||||||
sessionResponse="$(curl -skS -X POST "${API_URL}auth" --user-agent "Pi-hole cli " --data "{\"password\":\"${password}\"}" )"
|
sessionResponse="$(curl -skS -X POST "${API_URL}auth" --user-agent "Pi-hole cli " --data "{\"password\":\"${password}\"}" )"
|
||||||
|
|
||||||
if [ -z "${sessionResponse}" ]; then
|
if [ -z "${sessionResponse}" ]; then
|
||||||
|
@ -105,7 +126,7 @@ LoginAPI() {
|
||||||
SID=$(echo "${sessionResponse}"| jq --raw-output .session.sid 2>/dev/null)
|
SID=$(echo "${sessionResponse}"| jq --raw-output .session.sid 2>/dev/null)
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteSession() {
|
LogoutAPI() {
|
||||||
# if a valid Session exists (no password required or successful Authentication) and
|
# if a valid Session exists (no password required or successful Authentication) and
|
||||||
# SID is not null (successful Authentication only), delete the session
|
# SID is not null (successful Authentication only), delete the session
|
||||||
if [ "${validSession}" = true ] && [ ! "${SID}" = null ]; then
|
if [ "${validSession}" = true ] && [ ! "${SID}" = null ]; then
|
||||||
|
@ -113,7 +134,6 @@ DeleteSession() {
|
||||||
deleteResponse=$(curl -skS -o /dev/null -w "%{http_code}" -X DELETE "${API_URL}auth" -H "Accept: application/json" -H "sid: ${SID}")
|
deleteResponse=$(curl -skS -o /dev/null -w "%{http_code}" -X DELETE "${API_URL}auth" -H "Accept: application/json" -H "sid: ${SID}")
|
||||||
|
|
||||||
case "${deleteResponse}" in
|
case "${deleteResponse}" in
|
||||||
"204") printf "%b" "Session successfully deleted.\n";;
|
|
||||||
"401") printf "%b" "Logout attempt without a valid session. Unauthorized!\n";;
|
"401") printf "%b" "Logout attempt without a valid session. Unauthorized!\n";;
|
||||||
esac;
|
esac;
|
||||||
fi
|
fi
|
||||||
|
@ -142,6 +162,20 @@ GetFTLData() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PostFTLData() {
|
||||||
|
local data response status
|
||||||
|
# send the data to the API
|
||||||
|
response=$(curl -skS -w "%{http_code}" -X POST "${API_URL}$1" --data-raw "$2" -H "Accept: application/json" -H "sid: ${SID}" )
|
||||||
|
# data is everything from response without the last 3 characters
|
||||||
|
if [ "${3}" = "status" ]; then
|
||||||
|
# Keep the status code appended if requested
|
||||||
|
printf %s "${response}"
|
||||||
|
else
|
||||||
|
# Strip the status code
|
||||||
|
printf %s "${response%???}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
secretRead() {
|
secretRead() {
|
||||||
|
|
||||||
# POSIX compliant function to read user-input and
|
# POSIX compliant function to read user-input and
|
||||||
|
|
|
@ -5,261 +5,187 @@
|
||||||
# (c) 2017 Pi-hole, LLC (https://pi-hole.net)
|
# (c) 2017 Pi-hole, LLC (https://pi-hole.net)
|
||||||
# Network-wide ad blocking via your own hardware.
|
# Network-wide ad blocking via your own hardware.
|
||||||
#
|
#
|
||||||
# Whitelist and blacklist domains
|
# allowlist and denylist domains
|
||||||
#
|
#
|
||||||
# This file is copyright under the latest version of the EUPL.
|
# This file is copyright under the latest version of the EUPL.
|
||||||
# Please see LICENSE file for your rights under this license.
|
# Please see LICENSE file for your rights under this license.
|
||||||
|
|
||||||
# Globals
|
readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
|
||||||
piholeDir="/etc/pihole"
|
readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
|
||||||
GRAVITYDB="${piholeDir}/gravity.db"
|
source "${utilsfile}"
|
||||||
# Source pihole-FTL from install script
|
|
||||||
pihole_FTL="${piholeDir}/pihole-FTL.conf"
|
readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh"
|
||||||
if [[ -f "${pihole_FTL}" ]]; then
|
source "${apifile}"
|
||||||
source "${pihole_FTL}"
|
|
||||||
|
# Determine database location
|
||||||
|
DBFILE=$(getFTLConfigValue "files.database")
|
||||||
|
if [ -z "$DBFILE" ]; then
|
||||||
|
DBFILE="/etc/pihole/pihole-FTL.db"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set this only after sourcing pihole-FTL.conf as the gravity database path may
|
# Determine gravity database location
|
||||||
# have changed
|
GRAVITYDB=$(getFTLConfigValue "files.gravity")
|
||||||
gravityDBfile="${GRAVITYDB}"
|
if [ -z "$GRAVITYDB" ]; then
|
||||||
|
GRAVITYDB="/etc/pihole/gravity.db"
|
||||||
|
fi
|
||||||
|
|
||||||
noReloadRequested=false
|
|
||||||
addmode=true
|
addmode=true
|
||||||
verbose=true
|
verbose=true
|
||||||
wildcard=false
|
wildcard=false
|
||||||
web=false
|
|
||||||
|
|
||||||
domList=()
|
domList=()
|
||||||
|
|
||||||
typeId=""
|
typeId=""
|
||||||
comment=""
|
comment=""
|
||||||
declare -i domaincount
|
|
||||||
domaincount=0
|
|
||||||
reload=false
|
|
||||||
|
|
||||||
colfile="/opt/pihole/COL_TABLE"
|
colfile="/opt/pihole/COL_TABLE"
|
||||||
source ${colfile}
|
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() {
|
helpFunc() {
|
||||||
local listname param
|
echo "Usage: pihole ${abbrv} [options] <domain> <domain2 ...>
|
||||||
|
Example: 'pihole ${abbrv} site.com', or 'pihole ${abbrv} site1.com site2.com'
|
||||||
listname="$(GetListnameFromTypeId "${typeId}")"
|
${typeId^} one or more ${kindId} domains
|
||||||
param="$(GetListParamFromTypeId)"
|
|
||||||
|
|
||||||
echo "Usage: pihole -${param} [options] <domain> <domain2 ...>
|
|
||||||
Example: 'pihole -${param} site.com', or 'pihole -${param} site1.com site2.com'
|
|
||||||
${listname^} one or more domains
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d, --delmode Remove domain(s) from the ${listname}
|
remove, delete, -d Remove domain(s)
|
||||||
-nr, --noreload Update ${listname} without reloading the DNS server
|
|
||||||
-q, --quiet Make output less verbose
|
-q, --quiet Make output less verbose
|
||||||
-h, --help Show this help dialog
|
-h, --help Show this help dialog
|
||||||
-l, --list Display all your ${listname}listed domains
|
-l, --list Display domains
|
||||||
--nuke Removes all entries in a list
|
|
||||||
--comment \"text\" Add a comment to the domain. If adding multiple domains the same comment will be used for all"
|
--comment \"text\" Add a comment to the domain. If adding multiple domains the same comment will be used for all"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateDomain() {
|
CreateDomainList() {
|
||||||
# Convert to lowercase
|
|
||||||
domain="${1,,}"
|
|
||||||
local str validDomain
|
|
||||||
|
|
||||||
# Check validity of domain (don't check for regex entries)
|
|
||||||
if [[ ( "${typeId}" == "${regex_blacklist}" || "${typeId}" == "${regex_whitelist}" ) && "${wildcard}" == false ]]; then
|
|
||||||
validDomain="${domain}"
|
|
||||||
else
|
|
||||||
# Check max length
|
|
||||||
if [[ "${#domain}" -le 253 ]]; then
|
|
||||||
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
|
|
||||||
# set error string
|
|
||||||
str="is not a valid argument or domain name!"
|
|
||||||
else
|
|
||||||
validDomain=
|
|
||||||
str="is too long!"
|
|
||||||
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${validDomain}" ]]; then
|
|
||||||
domList=("${domList[@]}" "${validDomain}")
|
|
||||||
else
|
|
||||||
echo -e " ${CROSS} ${domain} ${str}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
domaincount=$((domaincount+1))
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessDomainList() {
|
|
||||||
for dom in "${domList[@]}"; do
|
|
||||||
# Format domain into regex filter if requested
|
# Format domain into regex filter if requested
|
||||||
|
local dom=${1}
|
||||||
if [[ "${wildcard}" == true ]]; then
|
if [[ "${wildcard}" == true ]]; then
|
||||||
dom="(\\.|^)${dom//\./\\.}$"
|
dom="(\\.|^)${dom//\./\\.}$"
|
||||||
fi
|
fi
|
||||||
|
domList=("${domList[@]}" "${dom}")
|
||||||
# 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}"
|
|
||||||
else
|
|
||||||
RemoveDomain "${dom}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddDomain() {
|
AddDomain() {
|
||||||
local domain num requestedListname existingTypeId existingListname
|
local json num data
|
||||||
domain="$1"
|
|
||||||
|
|
||||||
# Is the domain in the list we want to add it to?
|
# Authenticate with the API
|
||||||
num="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}';")"
|
LoginAPI
|
||||||
requestedListname="$(GetListnameFromTypeId "${typeId}")"
|
|
||||||
|
|
||||||
if [[ "${num}" -ne 0 ]]; then
|
# Prepare request to POST /api/domains/{type}/{kind}
|
||||||
existingTypeId="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT type FROM domainlist WHERE domain = '${domain}';")"
|
# Build JSON object of the following form
|
||||||
if [[ "${existingTypeId}" == "${typeId}" ]]; then
|
# {
|
||||||
if [[ "${verbose}" == true ]]; then
|
# "domain": [ <domains> ],
|
||||||
echo -e " ${INFO} ${1} already exists in ${requestedListname}, no need to add!"
|
# "comment": <comment>
|
||||||
|
# }
|
||||||
|
# where <domains> is an array of domain strings and <comment> is a string
|
||||||
|
# We use jq to build the JSON object
|
||||||
|
json=$(jq --null-input --compact-output --arg domains "${domList[*]}" --arg comment "${comment}" '{domain: $domains | split(" "), comment: $comment}')
|
||||||
|
|
||||||
|
# Send the request
|
||||||
|
data=$(PostFTLData "domains/${typeId}/${kindId}" "${json}")
|
||||||
|
|
||||||
|
# Display domain(s) added
|
||||||
|
# (they are listed in .processed.success, use jq)
|
||||||
|
num=$(echo "${data}" | jq '.processed.success | length')
|
||||||
|
if [[ "${num}" -gt 0 ]] && [[ "${verbose}" == true ]]; then
|
||||||
|
echo -e " ${TICK} Added ${num} domain(s):"
|
||||||
|
for i in $(seq 0 $((num-1))); do
|
||||||
|
echo -e " - ${COL_BLUE}$(echo "${data}" | jq --raw-output ".processed.success[$i].item")${COL_NC}"
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
else
|
# Display failed domain(s)
|
||||||
existingListname="$(GetListnameFromTypeId "${existingTypeId}")"
|
# (they are listed in .processed.errors, use jq)
|
||||||
pihole-FTL sqlite3 -ni "${gravityDBfile}" "UPDATE domainlist SET type = ${typeId} WHERE domain='${domain}';"
|
num=$(echo "${data}" | jq '.processed.errors | length')
|
||||||
if [[ "${verbose}" == true ]]; then
|
if [[ "${num}" -gt 0 ]] && [[ "${verbose}" == true ]]; then
|
||||||
echo -e " ${INFO} ${1} already exists in ${existingListname}, it has been moved to ${requestedListname}!"
|
echo -e " ${CROSS} Failed to add ${num} domain(s):"
|
||||||
|
for i in $(seq 0 $((num-1))); do
|
||||||
|
echo -e " - ${COL_BLUE}$(echo "${data}" | jq --raw-output ".processed.errors[$i].item")${COL_NC}"
|
||||||
|
error=$(echo "${data}" | jq --raw-output ".processed.errors[$i].error")
|
||||||
|
if [[ "${error}" == "UNIQUE constraint failed: domainlist.domain, domainlist.type" ]]; then
|
||||||
|
error="Domain already in the specified list"
|
||||||
fi
|
fi
|
||||||
fi
|
echo -e " ${error}"
|
||||||
return
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Domain not found in the table, add it!
|
# Log out
|
||||||
if [[ "${verbose}" == true ]]; then
|
LogoutAPI
|
||||||
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)
|
|
||||||
if [[ -z "${comment}" ]]; then
|
|
||||||
pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT INTO domainlist (domain,type) VALUES ('${domain}',${typeId});"
|
|
||||||
else
|
|
||||||
# also add comment when variable has been set through the "--comment" option
|
|
||||||
pihole-FTL sqlite3 -ni "${gravityDBfile}" "INSERT INTO domainlist (domain,type,comment) VALUES ('${domain}',${typeId},'${comment}');"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveDomain() {
|
RemoveDomain() {
|
||||||
local domain num requestedListname
|
local json num data status
|
||||||
domain="$1"
|
|
||||||
|
|
||||||
# Is the domain in the list we want to remove it from?
|
# Authenticate with the API
|
||||||
num="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(*) FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};")"
|
LoginAPI
|
||||||
|
|
||||||
requestedListname="$(GetListnameFromTypeId "${typeId}")"
|
# Prepare request to POST /api/domains:batchDelete
|
||||||
|
# Build JSON object of the following form
|
||||||
|
# [{
|
||||||
|
# "item": <domain>,
|
||||||
|
# "type": "${typeId}",
|
||||||
|
# "kind": "${kindId}",
|
||||||
|
# }]
|
||||||
|
# where <domain> is the domain string and ${typeId} and ${kindId} are the type and kind IDs
|
||||||
|
# We use jq to build the JSON object)
|
||||||
|
json=$(jq --null-input --compact-output --arg domains "${domList[*]}" --arg typeId "${typeId}" --arg kindId "${kindId}" '[ $domains | split(" ")[] as $item | {item: $item, type: $typeId, kind: $kindId} ]')
|
||||||
|
|
||||||
if [[ "${num}" -eq 0 ]]; then
|
# Send the request
|
||||||
if [[ "${verbose}" == true ]]; then
|
data=$(PostFTLData "domains:batchDelete" "${json}" "status")
|
||||||
echo -e " ${INFO} ${domain} does not exist in ${requestedListname}, no need to remove!"
|
# Separate the status from the data
|
||||||
fi
|
status=$(printf %s "${data#"${data%???}"}")
|
||||||
return
|
data=$(printf %s "${data%???}")
|
||||||
|
|
||||||
|
# If there is an .error object in the returned data, display it
|
||||||
|
local error
|
||||||
|
error=$(jq --compact-output <<< "${data}" '.error')
|
||||||
|
if [[ $error != "null" && $error != "" ]]; then
|
||||||
|
echo -e " ${CROSS} Failed to remove domain(s):"
|
||||||
|
echo -e " $(jq <<< "${data}" '.error')"
|
||||||
|
elif [[ "${verbose}" == true && "${status}" == "204" ]]; then
|
||||||
|
echo -e " ${TICK} Domain(s) removed from the ${kindId} ${typeId}list"
|
||||||
|
elif [[ "${verbose}" == true && "${status}" == "404" ]]; then
|
||||||
|
echo -e " ${TICK} Requested domain(s) not found on ${kindId} ${typeId}list"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Domain found in the table, remove it!
|
# Log out
|
||||||
if [[ "${verbose}" == true ]]; then
|
LogoutAPI
|
||||||
echo -e " ${INFO} Removing ${domain} from the ${requestedListname}..."
|
|
||||||
fi
|
|
||||||
reload=true
|
|
||||||
# Remove it from the current list
|
|
||||||
pihole-FTL sqlite3 -ni "${gravityDBfile}" "DELETE FROM domainlist WHERE domain = '${domain}' AND type = ${typeId};"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Displaylist() {
|
Displaylist() {
|
||||||
local count num_pipes domain enabled status nicedate requestedListname
|
local data
|
||||||
|
|
||||||
requestedListname="$(GetListnameFromTypeId "${typeId}")"
|
# if either typeId or kindId is empty, we cannot display the list
|
||||||
data="$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT domain,enabled,date_modified FROM domainlist WHERE type = ${typeId};" 2> /dev/null)"
|
if [[ -z "${typeId}" ]] || [[ -z "${kindId}" ]]; then
|
||||||
|
echo " ${CROSS} Unable to display list. Please specify a list type and kind."
|
||||||
if [[ -z $data ]]; then
|
exit 1
|
||||||
echo -e "Not showing empty list"
|
|
||||||
else
|
|
||||||
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
|
fi
|
||||||
|
|
||||||
# Get nice representation of numerical date stored in database
|
# Authenticate with the API
|
||||||
nicedate=$(date --rfc-2822 -d "@${datemod}")
|
LoginAPI
|
||||||
|
|
||||||
echo " ${count}: ${domain} (${status}, last modified ${nicedate})"
|
# Send the request
|
||||||
count=$((count+1))
|
data=$(GetFTLData "domains/${typeId}/${kindId}")
|
||||||
done <<< "${data}"
|
|
||||||
fi
|
|
||||||
exit 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
NukeList() {
|
# Display the list
|
||||||
count=$(pihole-FTL sqlite3 -ni "${gravityDBfile}" "SELECT COUNT(1) FROM domainlist WHERE type = ${typeId};")
|
num=$(echo "${data}" | jq '.domains | length')
|
||||||
listname="$(GetListnameFromTypeId "${typeId}")"
|
if [[ "${num}" -gt 0 ]]; then
|
||||||
if [ "$count" -gt 0 ];then
|
echo -e " ${TICK} Found ${num} domain(s) in the ${kindId} ${typeId}list:"
|
||||||
pihole-FTL sqlite3 -ni "${gravityDBfile}" "DELETE FROM domainlist WHERE type = ${typeId};"
|
for i in $(seq 0 $((num-1))); do
|
||||||
echo " ${TICK} Removed ${count} domain(s) from the ${listname}"
|
echo -e " - ${COL_BLUE}$(echo "${data}" | jq --compact-output ".domains[$i].domain")${COL_NC}"
|
||||||
|
echo -e " Comment: $(echo "${data}" | jq --compact-output ".domains[$i].comment")"
|
||||||
|
echo -e " Groups: $(echo "${data}" | jq --compact-output ".domains[$i].groups")"
|
||||||
|
echo -e " Added: $(date -d @"$(echo "${data}" | jq --compact-output ".domains[$i].date_added")")"
|
||||||
|
echo -e " Last modified: $(date -d @"$(echo "${data}" | jq --compact-output ".domains[$i].date_modified")")"
|
||||||
|
done
|
||||||
else
|
else
|
||||||
echo " ${INFO} ${listname} already empty. Nothing to do!"
|
echo -e " ${INFO} No domains found in the ${kindId} ${typeId}list"
|
||||||
fi
|
fi
|
||||||
exit 0;
|
|
||||||
|
# Log out
|
||||||
|
LogoutAPI
|
||||||
|
|
||||||
|
# Return early without adding/deleting domains
|
||||||
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
GetComment() {
|
GetComment() {
|
||||||
|
@ -272,38 +198,30 @@ GetComment() {
|
||||||
|
|
||||||
while (( "$#" )); do
|
while (( "$#" )); do
|
||||||
case "${1}" in
|
case "${1}" in
|
||||||
"-w" | "whitelist" ) typeId=0;;
|
"allow" | "allowlist" ) kindId="exact"; typeId="allow"; abbrv="allow";;
|
||||||
"-b" | "blacklist" ) typeId=1;;
|
"deny" | "denylist" ) kindId="exact"; typeId="deny"; abbrv="deny";;
|
||||||
"--white-regex" | "white-regex" ) typeId=2;;
|
"--allow-regex" | "allow-regex" ) kindId="regex"; typeId="allow"; abbrv="--allow-regex";;
|
||||||
"--white-wild" | "white-wild" ) typeId=2; wildcard=true;;
|
"--allow-wild" | "allow-wild" ) kindId="regex"; typeId="allow"; wildcard=true; abbrv="--allow-wild";;
|
||||||
"--wild" | "wildcard" ) typeId=3; wildcard=true;;
|
"--regex" | "regex" ) kindId="regex"; typeId="deny"; abbrv="--regex";;
|
||||||
"--regex" | "regex" ) typeId=3;;
|
"--wild" | "wildcard" ) kindId="regex"; typeId="deny"; wildcard=true; abbrv="--wild";;
|
||||||
"-nr"| "--noreload" ) noReloadRequested=true;;
|
"-d" | "remove" | "delete" ) addmode=false;;
|
||||||
"-d" | "--delmode" ) addmode=false;;
|
|
||||||
"-q" | "--quiet" ) verbose=false;;
|
"-q" | "--quiet" ) verbose=false;;
|
||||||
"-h" | "--help" ) helpFunc;;
|
"-h" | "--help" ) helpFunc;;
|
||||||
"-l" | "--list" ) Displaylist;;
|
"-l" | "--list" ) Displaylist;;
|
||||||
"--nuke" ) NukeList;;
|
|
||||||
"--web" ) web=true;;
|
|
||||||
"--comment" ) GetComment "${2}"; shift;;
|
"--comment" ) GetComment "${2}"; shift;;
|
||||||
* ) ValidateDomain "${1}";;
|
* ) CreateDomainList "${1}";;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
shift
|
shift
|
||||||
|
|
||||||
if [[ ${domaincount} == 0 ]]; then
|
if [[ ${#domList[@]} == 0 ]]; then
|
||||||
helpFunc
|
helpFunc
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ProcessDomainList
|
if ${addmode}; then
|
||||||
|
AddDomain
|
||||||
# Used on web interface
|
else
|
||||||
if $web; then
|
RemoveDomain
|
||||||
echo "DONE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${reload} == true && ${noReloadRequested} == false ]]; then
|
|
||||||
pihole restartdns reload-lists
|
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Pi-hole: A black hole for Internet advertisements
|
|
||||||
# (c) 2020 Pi-hole, LLC (https://pi-hole.net)
|
|
||||||
# Network-wide ad blocking via your own hardware.
|
|
||||||
#
|
|
||||||
# This file is copyright under the latest version of the EUPL.
|
|
||||||
# Please see LICENSE file for your rights under this license.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# The pihole disable command has the option to set a specified time before
|
|
||||||
# blocking is automatically re-enabled.
|
|
||||||
#
|
|
||||||
# Present script is responsible for the sleep & re-enable part of the job and
|
|
||||||
# is automatically terminated if it is still running when pihole is enabled by
|
|
||||||
# other means.
|
|
||||||
#
|
|
||||||
# This ensures that pihole ends up in the correct state after a sequence of
|
|
||||||
# commands suchs as: `pihole disable 30s; pihole enable; pihole disable`
|
|
||||||
|
|
||||||
readonly PI_HOLE_BIN_DIR="/usr/local/bin"
|
|
||||||
|
|
||||||
sleep "${1}"
|
|
||||||
"${PI_HOLE_BIN_DIR}"/pihole enable
|
|
|
@ -15,27 +15,29 @@ if [[ -f ${coltable} ]]; then
|
||||||
source ${coltable}
|
source ${coltable}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
|
||||||
|
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
|
||||||
|
source "${utilsfile}"
|
||||||
|
|
||||||
# Determine database location
|
# Determine database location
|
||||||
# Obtain DBFILE=... setting from pihole-FTL.db
|
DBFILE=$(getFTLConfigValue "files.database")
|
||||||
# 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
|
if [ -z "$DBFILE" ]; then
|
||||||
DBFILE="/etc/pihole/pihole-FTL.db"
|
DBFILE="/etc/pihole/pihole-FTL.db"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
flushARP(){
|
flushARP(){
|
||||||
local output
|
local output
|
||||||
if [[ "${args[1]}" != "quiet" ]]; then
|
if [[ "${args[1]}" != "quiet" ]]; then
|
||||||
echo -ne " ${INFO} Flushing network table ..."
|
echo -ne " ${INFO} Flushing network table ..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Stop FTL to prevent database access
|
||||||
|
if ! output=$(pihole-FTL service stop 2>&1); then
|
||||||
|
echo -e "${OVER} ${CROSS} Failed to stop FTL"
|
||||||
|
echo " Output: ${output}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Truncate network_addresses table in pihole-FTL.db
|
# Truncate network_addresses table in pihole-FTL.db
|
||||||
# This needs to be done before we can truncate the network table due to
|
# This needs to be done before we can truncate the network table due to
|
||||||
# foreign key constraints
|
# foreign key constraints
|
||||||
|
@ -54,6 +56,20 @@ flushARP(){
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Flush ARP cache of the host
|
||||||
|
if ! output=$(ip -s -s neigh flush all 2>&1); then
|
||||||
|
echo -e "${OVER} ${CROSS} Failed to flush ARP cache"
|
||||||
|
echo " Output: ${output}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start FTL again
|
||||||
|
if ! output=$(pihole-FTL service restart 2>&1); then
|
||||||
|
echo -e "${OVER} ${CROSS} Failed to restart FTL"
|
||||||
|
echo " Output: ${output}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${args[1]}" != "quiet" ]]; then
|
if [[ "${args[1]}" != "quiet" ]]; then
|
||||||
echo -e "${OVER} ${TICK} Flushed network table"
|
echo -e "${OVER} ${TICK} Flushed network table"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -11,32 +11,39 @@
|
||||||
colfile="/opt/pihole/COL_TABLE"
|
colfile="/opt/pihole/COL_TABLE"
|
||||||
source ${colfile}
|
source ${colfile}
|
||||||
|
|
||||||
|
readonly PI_HOLE_SCRIPT_DIR="/opt/pihole"
|
||||||
|
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
|
||||||
|
source "${utilsfile}"
|
||||||
|
|
||||||
# In case we're running at the same time as a system logrotate, use a
|
# In case we're running at the same time as a system logrotate, use a
|
||||||
# separate logrotate state file to prevent stepping on each other's
|
# separate logrotate state file to prevent stepping on each other's
|
||||||
# toes.
|
# toes.
|
||||||
STATEFILE="/var/lib/logrotate/pihole"
|
STATEFILE="/var/lib/logrotate/pihole"
|
||||||
|
|
||||||
# Determine database location
|
# Determine database location
|
||||||
# Obtain DBFILE=... setting from pihole-FTL.db
|
DBFILE=$(getFTLConfigValue "files.database")
|
||||||
# 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
|
if [ -z "$DBFILE" ]; then
|
||||||
DBFILE="/etc/pihole/pihole-FTL.db"
|
DBFILE="/etc/pihole/pihole-FTL.db"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$*" != *"quiet"* ]]; then
|
# Determine log file location
|
||||||
echo -ne " ${INFO} Flushing /var/log/pihole/pihole.log ..."
|
LOGFILE=$(getFTLConfigValue "files.log.dnsmasq")
|
||||||
|
if [ -z "$LOGFILE" ]; then
|
||||||
|
LOGFILE="/var/log/pihole/pihole.log"
|
||||||
fi
|
fi
|
||||||
|
FTLFILE=$(getFTLConfigValue "files.log.ftl")
|
||||||
|
if [ -z "$FTLFILE" ]; then
|
||||||
|
FTLFILE="/var/log/pihole/FTL.log"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$*" == *"once"* ]]; then
|
if [[ "$*" == *"once"* ]]; then
|
||||||
# Nightly logrotation
|
# Nightly logrotation
|
||||||
if command -v /usr/sbin/logrotate >/dev/null; then
|
if command -v /usr/sbin/logrotate >/dev/null; then
|
||||||
# Logrotate once
|
# Logrotate once
|
||||||
|
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -ne " ${INFO} Running logrotate ..."
|
||||||
|
fi
|
||||||
/usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate
|
/usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate
|
||||||
else
|
else
|
||||||
# Copy pihole.log over to pihole.log.1
|
# Copy pihole.log over to pihole.log.1
|
||||||
|
@ -44,32 +51,72 @@ if [[ "$*" == *"once"* ]]; then
|
||||||
# Note that moving the file is not an option, as
|
# Note that moving the file is not an option, as
|
||||||
# dnsmasq would happily continue writing into the
|
# dnsmasq would happily continue writing into the
|
||||||
# moved file (it will have the same file handler)
|
# moved file (it will have the same file handler)
|
||||||
cp -p /var/log/pihole/pihole.log /var/log/pihole/pihole.log.1
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
echo " " > /var/log/pihole/pihole.log
|
echo -ne " ${INFO} Rotating ${LOGFILE} ..."
|
||||||
chmod 640 /var/log/pihole/pihole.log
|
fi
|
||||||
|
cp -p "${LOGFILE}" "${LOGFILE}.1"
|
||||||
|
echo " " > "${LOGFILE}"
|
||||||
|
chmod 640 "${LOGFILE}"
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -e "${OVER} ${TICK} Rotated ${LOGFILE} ..."
|
||||||
|
fi
|
||||||
|
# Copy FTL.log over to FTL.log.1
|
||||||
|
# and empty out FTL.log
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -ne " ${INFO} Rotating ${FTLFILE} ..."
|
||||||
|
fi
|
||||||
|
cp -p "${FTLFILE}" "${FTLFILE}.1"
|
||||||
|
echo " " > "${FTLFILE}"
|
||||||
|
chmod 640 "${FTLFILE}"
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -e "${OVER} ${TICK} Rotated ${FTLFILE} ..."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Manual flushing
|
# Manual flushing
|
||||||
if command -v /usr/sbin/logrotate >/dev/null; then
|
|
||||||
# Logrotate twice to move all data out of sight of FTL
|
|
||||||
/usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate; sleep 3
|
|
||||||
/usr/sbin/logrotate --force --state "${STATEFILE}" /etc/pihole/logrotate
|
|
||||||
else
|
|
||||||
# Flush both pihole.log and pihole.log.1 (if existing)
|
|
||||||
echo " " > /var/log/pihole/pihole.log
|
|
||||||
if [ -f /var/log/pihole/pihole.log.1 ]; then
|
|
||||||
echo " " > /var/log/pihole/pihole.log.1
|
|
||||||
chmod 640 /var/log/pihole/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)
|
|
||||||
deleted=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1")
|
|
||||||
|
|
||||||
# Restart pihole-FTL to force reloading history
|
# Flush both pihole.log and pihole.log.1 (if existing)
|
||||||
sudo pihole restartdns
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -ne " ${INFO} Flushing ${LOGFILE} ..."
|
||||||
|
fi
|
||||||
|
echo " " > "${LOGFILE}"
|
||||||
|
chmod 640 "${LOGFILE}"
|
||||||
|
if [ -f "${LOGFILE}.1" ]; then
|
||||||
|
echo " " > "${LOGFILE}.1"
|
||||||
|
chmod 640 "${LOGFILE}.1"
|
||||||
|
fi
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -e "${OVER} ${TICK} Flushed ${LOGFILE} ..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Flush both FTL.log and FTL.log.1 (if existing)
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -ne " ${INFO} Flushing ${FTLFILE} ..."
|
||||||
|
fi
|
||||||
|
echo " " > "${FTLFILE}"
|
||||||
|
chmod 640 "${FTLFILE}"
|
||||||
|
if [ -f "${FTLFILE}.1" ]; then
|
||||||
|
echo " " > "${FTLFILE}.1"
|
||||||
|
chmod 640 "${FTLFILE}.1"
|
||||||
|
fi
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -e "${OVER} ${TICK} Flushed ${FTLFILE} ..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$*" != *"quiet"* ]]; then
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
echo -e "${OVER} ${TICK} Flushed /var/log/pihole/pihole.log"
|
echo -ne " ${INFO} Flushing database, DNS resolution temporarily unavailable ..."
|
||||||
echo -e " ${TICK} Deleted ${deleted} queries from database"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Stop FTL to make sure it doesn't write to the database while we're deleting data
|
||||||
|
service pihole-FTL stop
|
||||||
|
|
||||||
|
# Delete most recent 24 hours from FTL's database, leave even older data intact (don't wipe out all history)
|
||||||
|
deleted=$(pihole-FTL sqlite3 -ni "${DBFILE}" "DELETE FROM query_storage WHERE timestamp >= strftime('%s','now')-86400; select changes() from query_storage limit 1")
|
||||||
|
|
||||||
|
# Restart FTL
|
||||||
|
service pihole-FTL restart
|
||||||
|
if [[ "$*" != *"quiet"* ]]; then
|
||||||
|
echo -e "${OVER} ${TICK} Deleted ${deleted} queries from long-term query database"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,12 @@ GenerateOutput() {
|
||||||
printf "\n\n"
|
printf "\n\n"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# If no exact results were found, suggest using partial matching
|
||||||
|
if [ "${num_lists}" -eq 0 ] && [ "${num_gravity}" -eq 0 ] && [ "${partial}" = false ]; then
|
||||||
|
printf "%s\n" "Hint: Try partial matching with"
|
||||||
|
printf "%s\n\n" " ${COL_GREEN}pihole -q --partial ${domain}${COL_NC}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
Main() {
|
Main() {
|
||||||
|
@ -125,25 +131,16 @@ Main() {
|
||||||
# https://github.com/pi-hole/FTL/pull/1715
|
# https://github.com/pi-hole/FTL/pull/1715
|
||||||
# no need to do it here
|
# no need to do it here
|
||||||
|
|
||||||
# Test if the authentication endpoint is available
|
# Authenticate with FTL
|
||||||
TestAPIAvailability
|
LoginAPI
|
||||||
|
|
||||||
# Users can configure FTL in a way, that for accessing a) all endpoints (webserver.api.localAPIauth)
|
|
||||||
# or b) for the /search endpoint (webserver.api.searchAPIauth) no authentication is required.
|
|
||||||
# Therefore, we try to query directly without authentication but do authenticat if 401 is returned
|
|
||||||
|
|
||||||
data=$(GetFTLData "search/${domain}?N=${max_results}&partial=${partial}")
|
|
||||||
|
|
||||||
if [ "${data}" = 401 ]; then
|
|
||||||
# Unauthenticated, so authenticate with the FTL server required
|
|
||||||
Authentication
|
|
||||||
|
|
||||||
# send query again
|
# send query again
|
||||||
data=$(GetFTLData "search/${domain}?N=${max_results}&partial=${partial}")
|
data=$(GetFTLData "search/${domain}?N=${max_results}&partial=${partial}")
|
||||||
fi
|
|
||||||
|
|
||||||
GenerateOutput "${data}"
|
GenerateOutput "${data}"
|
||||||
DeleteSession
|
|
||||||
|
# Delete session
|
||||||
|
LogoutAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
# Process all options (if present)
|
# Process all options (if present)
|
||||||
|
|
|
@ -7,11 +7,11 @@ _pihole() {
|
||||||
|
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
"pihole")
|
"pihole")
|
||||||
opts="blacklist checkout debug disable enable flush help logging query reconfigure regex restartdns status tail uninstall updateGravity updatePihole version wildcard whitelist arpflush"
|
opts="allow allow-regex allow-wild deny checkout debug disable enable flush help logging query reconfigure regex restartdns status tail uninstall updateGravity updatePihole version wildcard arpflush"
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||||
;;
|
;;
|
||||||
"whitelist"|"blacklist"|"wildcard"|"regex")
|
"allow"|"deny"|"wildcard"|"regex"|"allow-regx"|"allow-wild")
|
||||||
opts_lists="\--delmode \--noreload \--quiet \--list \--nuke"
|
opts_lists="\not \--delmode \--quiet \--list \--help"
|
||||||
COMPREPLY=( $(compgen -W "${opts_lists}" -- ${cur}) )
|
COMPREPLY=( $(compgen -W "${opts_lists}" -- ${cur}) )
|
||||||
;;
|
;;
|
||||||
"checkout")
|
"checkout")
|
||||||
|
|
|
@ -52,48 +52,44 @@ pihole restartdns\fR [options]
|
||||||
Available commands and options:
|
Available commands and options:
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fB-w, whitelist\fR [options] [<domain1> <domain2 ...>]
|
\fBallow, allowlist\fR [options] [<domain1> <domain2 ...>]
|
||||||
.br
|
.br
|
||||||
Adds or removes specified domain or domains to the Whitelist
|
Adds or removes specified domain or domains to the Allowlist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fB-b, blacklist\fR [options] [<domain1> <domain2 ...>]
|
\fBdeny, denylist\fR [options] [<domain1> <domain2 ...>]
|
||||||
.br
|
.br
|
||||||
Adds or removes specified domain or domains to the blacklist
|
Adds or removes specified domain or domains to the denylist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fB--regex, regex\fR [options] [<regex1> <regex2 ...>]
|
\fB--regex, regex\fR [options] [<regex1> <regex2 ...>]
|
||||||
.br
|
.br
|
||||||
Add or removes specified regex filter to the regex blacklist
|
Add or removes specified regex filter to the regex denylist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fB--white-regex\fR [options] [<regex1> <regex2 ...>]
|
\fB--allow-regex\fR [options] [<regex1> <regex2 ...>]
|
||||||
.br
|
.br
|
||||||
Add or removes specified regex filter to the regex whitelist
|
Add or removes specified regex filter to the regex allowlist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fB--wild, wildcard\fR [options] [<domain1> <domain2 ...>]
|
\fB--wild, wildcard\fR [options] [<domain1> <domain2 ...>]
|
||||||
.br
|
.br
|
||||||
Add or removes specified domain to the wildcard blacklist
|
Add or removes specified domain to the wildcard denylist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fB--white-wild\fR [options] [<domain1> <domain2 ...>]
|
\fB--allow-wild\fR [options] [<domain1> <domain2 ...>]
|
||||||
.br
|
.br
|
||||||
Add or removes specified domain to the wildcard whitelist
|
Add or removes specified domain to the wildcard allowlist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
(Whitelist/Blacklist manipulation options):
|
(Allow-/denylist manipulation options):
|
||||||
.br
|
.br
|
||||||
-d, --delmode Remove domain(s) from the list
|
not, -d, --delmode Remove domain(s) from the list
|
||||||
.br
|
|
||||||
-nr, --noreload Update list without refreshing dnsmasq
|
|
||||||
.br
|
.br
|
||||||
-q, --quiet Make output less verbose
|
-q, --quiet Make output less verbose
|
||||||
.br
|
.br
|
||||||
-l, --list Display all your listed domains
|
-l, --list Display all your listed domains
|
||||||
.br
|
.br
|
||||||
--nuke Removes all entries in a list
|
|
||||||
.br
|
|
||||||
|
|
||||||
\fB-d, debug\fR [-a]
|
\fB-d, debug\fR [-a]
|
||||||
.br
|
.br
|
||||||
|
@ -279,17 +275,17 @@ Available commands and options:
|
||||||
Some usage examples
|
Some usage examples
|
||||||
.br
|
.br
|
||||||
|
|
||||||
Whitelist/blacklist manipulation
|
Allow-/denylist manipulation
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fBpihole -w iloveads.example.com\fR
|
\fBpihole allow iloveads.example.com\fR
|
||||||
.br
|
.br
|
||||||
Adds "iloveads.example.com" to whitelist
|
Allow "iloveads.example.com"
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fBpihole -b -d noads.example.com\fR
|
\fBpihole deny not noads.example.com\fR
|
||||||
.br
|
.br
|
||||||
Removes "noads.example.com" from blacklist
|
Removes "noads.example.com" from denylist
|
||||||
.br
|
.br
|
||||||
|
|
||||||
\fBpihole --wild example.com\fR
|
\fBpihole --wild example.com\fR
|
||||||
|
|
161
pihole
161
pihole
|
@ -19,9 +19,13 @@ PI_HOLE_BIN_DIR="/usr/local/bin"
|
||||||
readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
|
readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE"
|
||||||
source "${colfile}"
|
source "${colfile}"
|
||||||
|
|
||||||
utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
|
readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh"
|
||||||
source "${utilsfile}"
|
source "${utilsfile}"
|
||||||
|
|
||||||
|
# Source api functions
|
||||||
|
readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh"
|
||||||
|
source "${apifile}"
|
||||||
|
|
||||||
versionsfile="/etc/pihole/versions"
|
versionsfile="/etc/pihole/versions"
|
||||||
if [ -f "${versionsfile}" ]; then
|
if [ -f "${versionsfile}" ]; then
|
||||||
# Only source versionsfile if the file exits
|
# Only source versionsfile if the file exits
|
||||||
|
@ -205,73 +209,60 @@ restartDNS() {
|
||||||
|
|
||||||
piholeEnable() {
|
piholeEnable() {
|
||||||
if [[ "${2}" == "-h" ]] || [[ "${2}" == "--help" ]]; then
|
if [[ "${2}" == "-h" ]] || [[ "${2}" == "--help" ]]; then
|
||||||
echo "Usage: pihole disable [time]
|
echo "Usage: pihole enable/disable [time]
|
||||||
Example: 'pihole disable', or 'pihole disable 5m'
|
Example: 'pihole enable', or 'pihole disable 5m'
|
||||||
Disable Pi-hole subsystems
|
En- or disable Pi-hole subsystems
|
||||||
|
|
||||||
Time:
|
Time:
|
||||||
#s Disable Pi-hole functionality for # second(s)
|
#s En-/disable Pi-hole functionality for # second(s)
|
||||||
#m Disable Pi-hole functionality for # minute(s)"
|
#m En-/disable Pi-hole functionality for # minute(s)"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
elif [[ "${1}" == "0" ]]; then
|
|
||||||
# Disable Pi-hole
|
|
||||||
if ! getFTLConfigValue dns.blocking.active; then
|
|
||||||
echo -e " ${INFO} Blocking already disabled, nothing to do"
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Get timer
|
||||||
|
local tt="null"
|
||||||
if [[ $# -gt 1 ]]; then
|
if [[ $# -gt 1 ]]; then
|
||||||
local error=false
|
local error=false
|
||||||
if [[ "${2}" == *"s" ]]; then
|
if [[ "${2}" == *"s" ]]; then
|
||||||
tt=${2%"s"}
|
tt=${2%"s"}
|
||||||
if [[ "${tt}" =~ ^-?[0-9]+$ ]];then
|
if [[ ! "${tt}" =~ ^-?[0-9]+$ ]];then
|
||||||
local str="Disabling blocking for ${tt} seconds"
|
|
||||||
echo -e " ${INFO} ${str}..."
|
|
||||||
local str="Blocking will be re-enabled in ${tt} seconds"
|
|
||||||
nohup "${PI_HOLE_SCRIPT_DIR}"/pihole-reenable.sh ${tt} </dev/null &>/dev/null &
|
|
||||||
else
|
|
||||||
local error=true
|
local error=true
|
||||||
fi
|
fi
|
||||||
elif [[ "${2}" == *"m" ]]; then
|
elif [[ "${2}" == *"m" ]]; then
|
||||||
tt=${2%"m"}
|
tt=${2%"m"}
|
||||||
if [[ "${tt}" =~ ^-?[0-9]+$ ]];then
|
if [[ "${tt}" =~ ^-?[0-9]+$ ]];then
|
||||||
local str="Disabling blocking for ${tt} minutes"
|
|
||||||
echo -e " ${INFO} ${str}..."
|
|
||||||
local str="Blocking will be re-enabled in ${tt} minutes"
|
|
||||||
tt=$((${tt}*60))
|
tt=$((${tt}*60))
|
||||||
nohup "${PI_HOLE_SCRIPT_DIR}"/pihole-reenable.sh ${tt} </dev/null &>/dev/null &
|
|
||||||
else
|
else
|
||||||
local error=true
|
local error=true
|
||||||
fi
|
fi
|
||||||
elif [[ -n "${2}" ]]; then
|
elif [[ -n "${2}" ]]; then
|
||||||
local error=true
|
local error=true
|
||||||
else
|
|
||||||
echo -e " ${INFO} Disabling blocking"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${error} == true ]];then
|
if [[ ${error} == true ]];then
|
||||||
echo -e " ${COL_LIGHT_RED}Unknown format for delayed reactivation of the blocking!${COL_NC}"
|
echo -e " ${COL_LIGHT_RED}Unknown format for blocking timer!${COL_NC}"
|
||||||
echo -e " Try 'pihole disable --help' for more information."
|
echo -e " Try 'pihole disable --help' for more information."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local str="Pi-hole Disabled"
|
|
||||||
setFTLConfigValue dns.blocking.active false
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Enable Pi-hole
|
|
||||||
killall -q pihole-reenable
|
|
||||||
if getFTLConfigValue dns.blocking.active; then
|
|
||||||
echo -e " ${INFO} Blocking already enabled, nothing to do"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo -e " ${INFO} Enabling blocking"
|
|
||||||
local str="Pi-hole Enabled"
|
|
||||||
|
|
||||||
setFTLConfigValue dns.blocking.active true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
restartDNS reload-lists
|
# Authenticate with the API
|
||||||
|
LoginAPI
|
||||||
|
|
||||||
|
# Send the request
|
||||||
|
data=$(PostFTLData "dns/blocking" "{ \"blocking\": ${1}, \"timer\": ${tt} }")
|
||||||
|
|
||||||
|
# Check the response
|
||||||
|
local extra=" forever"
|
||||||
|
local timer="$(echo "${data}"| jq --raw-output '.timer' )"
|
||||||
|
if [[ "${timer}" != "null" ]]; then
|
||||||
|
extra=" for ${timer}s"
|
||||||
|
fi
|
||||||
|
local str="Pi-hole $(echo "${data}" | jq --raw-output '.blocking')${extra}"
|
||||||
|
|
||||||
|
# Logout from the API
|
||||||
|
LogoutAPI
|
||||||
|
|
||||||
echo -e "${OVER} ${TICK} ${str}"
|
echo -e "${OVER} ${TICK} ${str}"
|
||||||
}
|
}
|
||||||
|
@ -391,21 +382,22 @@ exit 0
|
||||||
|
|
||||||
tailFunc() {
|
tailFunc() {
|
||||||
# Warn user if Pi-hole's logging is disabled
|
# Warn user if Pi-hole's logging is disabled
|
||||||
local logging_enabled=$(grep -c "^log-queries" /etc/dnsmasq.d/01-pihole.conf)
|
local logging_enabled=$(getFTLConfigValue dns.queryLogging)
|
||||||
if [[ "${logging_enabled}" == "0" ]]; then
|
if [[ "${logging_enabled}" != "true" ]]; then
|
||||||
# No "log-queries" lines are found.
|
|
||||||
# Commented out lines (such as "#log-queries") are ignored
|
|
||||||
echo " ${CROSS} Warning: Query logging is disabled"
|
echo " ${CROSS} Warning: Query logging is disabled"
|
||||||
fi
|
fi
|
||||||
echo -e " ${INFO} Press Ctrl-C to exit"
|
echo -e " ${INFO} Press Ctrl-C to exit"
|
||||||
|
|
||||||
|
# Get logfile path
|
||||||
|
readonly LOGFILE=$(getFTLConfigValue files.log.dnsmasq)
|
||||||
|
|
||||||
# Strip date from each line
|
# Strip date from each line
|
||||||
# Color blocklist/blacklist/wildcard entries as red
|
# Color blocklist/denylist/wildcard entries as red
|
||||||
# Color A/AAAA/DHCP strings as white
|
# Color A/AAAA/DHCP strings as white
|
||||||
# Color everything else as gray
|
# Color everything else as gray
|
||||||
tail -f /var/log/pihole/pihole.log | grep --line-buffered "${1}" | sed -E \
|
tail -f $LOGFILE | grep --line-buffered "${1}" | sed -E \
|
||||||
-e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \
|
-e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \
|
||||||
-e "s,(.*(blacklisted |gravity blocked ).*),${COL_RED}&${COL_NC}," \
|
-e "s,(.*(denied |gravity blocked ).*),${COL_RED}&${COL_NC}," \
|
||||||
-e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \
|
-e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \
|
||||||
-e "s,.*,${COL_GRAY}&${COL_NC},"
|
-e "s,.*,${COL_GRAY}&${COL_NC},"
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -471,17 +463,17 @@ unsupportedFunc(){
|
||||||
|
|
||||||
helpFunc() {
|
helpFunc() {
|
||||||
echo "Usage: pihole [options]
|
echo "Usage: pihole [options]
|
||||||
Example: 'pihole -w -h'
|
Example: 'pihole allow -h'
|
||||||
Add '-h' after specific commands for more information on usage
|
Add '-h' after specific commands for more information on usage
|
||||||
|
|
||||||
Whitelist/Blacklist Options:
|
Domain Options:
|
||||||
-w, whitelist Whitelist domain(s)
|
allow, allowlist Allow domain(s)
|
||||||
-b, blacklist Blacklist domain(s)
|
deny, denylist Deny domain(s)
|
||||||
--regex, regex Regex blacklist domains(s)
|
--regex, regex Regex deny domains(s)
|
||||||
--white-regex Regex whitelist domains(s)
|
--allow-regex Regex allow domains(s)
|
||||||
--wild, wildcard Wildcard blacklist domain(s)
|
--wild, wildcard Wildcard deny domain(s)
|
||||||
--white-wild Wildcard whitelist domain(s)
|
--allow-wild Wildcard allow domain(s)
|
||||||
Add '-h' for more info on whitelist/blacklist usage
|
Add '-h' for more info on allow/deny usage
|
||||||
|
|
||||||
Debugging Options:
|
Debugging Options:
|
||||||
-d, debug Start a debugging session
|
-d, debug Start a debugging session
|
||||||
|
@ -536,23 +528,23 @@ case "${1}" in
|
||||||
"tricorder" ) tricorderFunc;;
|
"tricorder" ) tricorderFunc;;
|
||||||
|
|
||||||
# we need to add all arguments that require sudo power to not trigger the * argument
|
# we need to add all arguments that require sudo power to not trigger the * argument
|
||||||
"-w" | "whitelist" ) ;;
|
"allow" | "allowlist" ) need_root=0;;
|
||||||
"-b" | "blacklist" ) ;;
|
"deny" | "denylist" ) need_root=0;;
|
||||||
"--wild" | "wildcard" ) ;;
|
"--wild" | "wildcard" ) need_root=0;;
|
||||||
"--regex" | "regex" ) ;;
|
"--regex" | "regex" ) need_root=0;;
|
||||||
"--white-regex" | "white-regex" ) ;;
|
"--allow-regex" | "allow-regex" ) need_root=0;;
|
||||||
"--white-wild" | "white-wild" ) ;;
|
"--allow-wild" | "allow-wild" ) need_root=0;;
|
||||||
"-f" | "flush" ) ;;
|
"-f" | "flush" ) ;;
|
||||||
"-up" | "updatePihole" ) ;;
|
"-up" | "updatePihole" ) ;;
|
||||||
"-r" | "reconfigure" ) ;;
|
"-r" | "reconfigure" ) ;;
|
||||||
"-l" | "logging" ) ;;
|
"-l" | "logging" ) ;;
|
||||||
"uninstall" ) ;;
|
"uninstall" ) ;;
|
||||||
"enable" ) ;;
|
"enable" ) need_root=0;;
|
||||||
"disable" ) ;;
|
"disable" ) need_root=0;;
|
||||||
"-d" | "debug" ) ;;
|
"-d" | "debug" ) ;;
|
||||||
"restartdns" ) ;;
|
"restartdns" ) ;;
|
||||||
"-g" | "updateGravity" ) need_root=0;;
|
"-g" | "updateGravity" ) ;;
|
||||||
"reloaddns" ) need_root=0;;
|
"reloaddns" ) ;;
|
||||||
"setpassword" ) ;;
|
"setpassword" ) ;;
|
||||||
"checkout" ) ;;
|
"checkout" ) ;;
|
||||||
"updatechecker" ) ;;
|
"updatechecker" ) ;;
|
||||||
|
@ -561,42 +553,28 @@ case "${1}" in
|
||||||
* ) helpFunc;;
|
* ) helpFunc;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Must be root to use this tool for most functions
|
|
||||||
if [[ ! $EUID -eq 0 && need_root -eq 1 ]];then
|
|
||||||
if [[ -x "$(command -v sudo)" ]]; then
|
|
||||||
exec sudo bash "$0" "$@"
|
|
||||||
exit $?
|
|
||||||
else
|
|
||||||
echo -e " ${CROSS} sudo is needed to run pihole commands. Please run this script as root or install sudo."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# In the case of alpine running in a container, the USER variable appears to be blank
|
# In the case of alpine running in a container, the USER variable appears to be blank
|
||||||
# which prevents the next trap from working correctly. Set it by running whoami
|
# which prevents the next trap from working correctly. Set it by running whoami
|
||||||
if [[ -z ${USER} ]]; then
|
if [[ -z ${USER} ]]; then
|
||||||
USER=$(whoami)
|
USER=$(whoami)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Can also be user pihole for other functions
|
# Check if the current user is neither root nor pihole and if the command
|
||||||
if [[ ${USER} != "pihole" && need_root -eq 0 ]];then
|
# requires root. If so, exit with an error message.
|
||||||
if [[ -x "$(command -v sudo)" ]]; then
|
if [[ $EUID -ne 0 && ${USER} != "pihole" && need_root -eq 1 ]];then
|
||||||
exec sudo -u pihole bash "$0" "$@"
|
echo -e " ${CROSS} The Pi-hole command requires root privileges, try:"
|
||||||
exit $?
|
echo -e " ${COL_GREEN}sudo pihole $*${COL_NC}"
|
||||||
else
|
|
||||||
echo -e " ${CROSS} sudo is needed to run pihole commands. Please run this script as root or install sudo."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Handle redirecting to specific functions based on arguments
|
# Handle redirecting to specific functions based on arguments
|
||||||
case "${1}" in
|
case "${1}" in
|
||||||
"-w" | "whitelist" ) listFunc "$@";;
|
"allow" | "allowlist" ) listFunc "$@";;
|
||||||
"-b" | "blacklist" ) listFunc "$@";;
|
"deny" | "denylist" ) listFunc "$@";;
|
||||||
"--wild" | "wildcard" ) listFunc "$@";;
|
"--wild" | "wildcard" ) listFunc "$@";;
|
||||||
"--regex" | "regex" ) listFunc "$@";;
|
"--regex" | "regex" ) listFunc "$@";;
|
||||||
"--white-regex" | "white-regex" ) listFunc "$@";;
|
"--allow-regex" | "allow-regex" ) listFunc "$@";;
|
||||||
"--white-wild" | "white-wild" ) listFunc "$@";;
|
"--allow-wild" | "allow-wild" ) listFunc "$@";;
|
||||||
"-d" | "debug" ) debugFunc "$@";;
|
"-d" | "debug" ) debugFunc "$@";;
|
||||||
"-f" | "flush" ) flushFunc "$@";;
|
"-f" | "flush" ) flushFunc "$@";;
|
||||||
"-up" | "updatePihole" ) updatePiholeFunc "$@";;
|
"-up" | "updatePihole" ) updatePiholeFunc "$@";;
|
||||||
|
@ -604,8 +582,8 @@ case "${1}" in
|
||||||
"-g" | "updateGravity" ) updateGravityFunc "$@";;
|
"-g" | "updateGravity" ) updateGravityFunc "$@";;
|
||||||
"-l" | "logging" ) piholeLogging "$@";;
|
"-l" | "logging" ) piholeLogging "$@";;
|
||||||
"uninstall" ) uninstallFunc;;
|
"uninstall" ) uninstallFunc;;
|
||||||
"enable" ) piholeEnable 1;;
|
"enable" ) piholeEnable true "$2";;
|
||||||
"disable" ) piholeEnable 0 "$2";;
|
"disable" ) piholeEnable false "$2";;
|
||||||
"restartdns" ) restartDNS "$2";;
|
"restartdns" ) restartDNS "$2";;
|
||||||
"reloaddns" ) restartDNS "reload";;
|
"reloaddns" ) restartDNS "reload";;
|
||||||
"setpassword" ) SetWebPassword "$@";;
|
"setpassword" ) SetWebPassword "$@";;
|
||||||
|
@ -613,4 +591,5 @@ case "${1}" in
|
||||||
"updatechecker" ) shift; updateCheckFunc "$@";;
|
"updatechecker" ) shift; updateCheckFunc "$@";;
|
||||||
"arpflush" ) arpFunc "$@";;
|
"arpflush" ) arpFunc "$@";;
|
||||||
"-t" | "tail" ) tailFunc "$2";;
|
"-t" | "tail" ) tailFunc "$2";;
|
||||||
|
* ) helpFunc;;
|
||||||
esac
|
esac
|
||||||
|
|
Loading…
Reference in a new issue