761 lines
30 KiB
Plaintext
761 lines
30 KiB
Plaintext
|
#!/bin/sh
|
||
|
|
||
|
set -eu
|
||
|
export LC_ALL='C'
|
||
|
|
||
|
# Metadata.
|
||
|
if [ -z "${HBLOCK_VERSION+x}" ]; then HBLOCK_VERSION='3.4.1'; fi
|
||
|
if [ -z "${HBLOCK_AUTHOR+x}" ]; then HBLOCK_AUTHOR='Héctor Molinero Fernández <hector@molinero.dev>'; fi
|
||
|
if [ -z "${HBLOCK_LICENSE+x}" ]; then HBLOCK_LICENSE='MIT, https://opensource.org/licenses/MIT'; fi
|
||
|
if [ -z "${HBLOCK_REPOSITORY+x}" ]; then HBLOCK_REPOSITORY='https://github.com/hectorm/hblock'; fi
|
||
|
|
||
|
# Emulate ksh if the shell is zsh.
|
||
|
if [ -n "${ZSH_VERSION-}" ]; then emulate -L ksh; fi
|
||
|
|
||
|
# Define system and user configuration directories.
|
||
|
if [ -z "${ETCDIR+x}" ]; then ETCDIR='/etc'; fi
|
||
|
if [ -z "${XDG_CONFIG_HOME+x}" ]; then XDG_CONFIG_HOME="${HOME-}/.config"; fi
|
||
|
|
||
|
# Remove temporary files on exit.
|
||
|
cleanup() { ret="$?"; rm -rf -- "${TMPDIR:-${TMP:-/tmp}}/hblock.${$}."*; trap - EXIT; exit "${ret:?}"; }
|
||
|
{ trap cleanup EXIT ||:; trap cleanup TERM ||:; trap cleanup INT ||:; trap cleanup HUP ||:; } 2>/dev/null
|
||
|
|
||
|
# Built-in header.
|
||
|
HOSTNAME="${HOSTNAME-"$(uname -n)"}"
|
||
|
HBLOCK_HEADER_BUILTIN="$(cat <<-EOF
|
||
|
127.0.0.1 localhost ${HOSTNAME?}
|
||
|
255.255.255.255 broadcasthost
|
||
|
::1 localhost ${HOSTNAME?}
|
||
|
::1 ip6-localhost ip6-loopback
|
||
|
fe00::0 ip6-localnet
|
||
|
ff00::0 ip6-mcastprefix
|
||
|
ff02::1 ip6-allnodes
|
||
|
ff02::2 ip6-allrouters
|
||
|
ff02::3 ip6-allhosts
|
||
|
EOF
|
||
|
)"
|
||
|
|
||
|
# Built-in footer.
|
||
|
HBLOCK_FOOTER_BUILTIN=''
|
||
|
|
||
|
# Built-in sources.
|
||
|
HBLOCK_SOURCES_BUILTIN="$(cat <<-'EOF'
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/adaway.org/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/adblock-nocoin-list/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/adguard-cname-trackers/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/adguard-simplified/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/dandelionsprout-nordic/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-ara/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-bul/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-ces-slk/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-deu/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-fra/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-heb/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-ind/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-ita/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-kor/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-lav/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-lit/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-nld/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-por/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-rus/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-spa/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easylist-zho/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/easyprivacy/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/eth-phishing-detect/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/gfrogeye-firstparty-trackers/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/hostsvn/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/kadhosts/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/matomo.org-spammers/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/mitchellkrogza-badd-boyz-hosts/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/phishing.army/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/socram8888-notonmyshift/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/someonewhocares.org/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/spam404.com/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/stevenblack/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/ublock/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/ublock-abuse/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/ublock-badware/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/ublock-privacy/list.txt
|
||
|
https://raw.githubusercontent.com/hectorm/hmirror/master/data/urlhaus/list.txt
|
||
|
EOF
|
||
|
)"
|
||
|
|
||
|
# Built-in allowlist.
|
||
|
HBLOCK_ALLOWLIST_BUILTIN=''
|
||
|
|
||
|
# Built-in denylist.
|
||
|
HBLOCK_DENYLIST_BUILTIN="$(cat <<-'EOF'
|
||
|
# Special domain that is used to check if hBlock is enabled.
|
||
|
hblock-check.molinero.dev
|
||
|
EOF
|
||
|
)"
|
||
|
|
||
|
# Parse command line options.
|
||
|
optParse() {
|
||
|
SEP="$(printf '\037')"; POS=''
|
||
|
while [ "${#}" -gt '0' ]; do
|
||
|
case "${1?}" in
|
||
|
# Short options that accept a value need a "*" in their pattern because they can be found in the "-A<value>" form.
|
||
|
'-O'*|'--output') optArgStr "${@-}"; outputFile="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-H'*|'--header') optArgStr "${@-}"; headerFile="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-F'*|'--footer') optArgStr "${@-}"; footerFile="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-S'*|'--sources') optArgStr "${@-}"; sourcesFile="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-A'*|'--allowlist') optArgStr "${@-}"; allowlistFile="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-D'*|'--denylist') optArgStr "${@-}"; denylistFile="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-R'*|'--redirection') optArgStr "${@-}"; redirection="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-W'*|'--wrap') optArgStr "${@-}"; wrap="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-T'*|'--template') optArgStr "${@-}"; template="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-C'*|'--comment') optArgStr "${@-}"; comment="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-l' |'--lenient'|'--no-lenient') optArgBool "${@-}"; lenient="${optArg:?}" ;;
|
||
|
'-r' |'--regex'|'--no-regex') optArgBool "${@-}"; regex="${optArg:?}" ;;
|
||
|
'-f' |'--filter-subdomains'|'--no-filter-subdomains') optArgBool "${@-}"; filterSubdomains="${optArg:?}" ;;
|
||
|
'-c' |'--continue'|'--no-continue') optArgBool "${@-}"; continue="${optArg:?}" ;;
|
||
|
'-p'*|'--parallel') optArgStr "${@-}"; parallel="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-q' |'--quiet'|'--no-quiet') optArgBool "${@-}"; quiet="${optArg:?}" ;;
|
||
|
'-x'*|'--color') optArgStr "${@-}"; color="${optArg?}"; shift "${optShift:?}" ;;
|
||
|
'-v' |'--version') showVersion ;;
|
||
|
'-h' |'--help') showHelp ;;
|
||
|
# If "--" is found, the remaining positional parameters are saved and the parsing ends.
|
||
|
--) shift; _IFS="${IFS?}"; IFS="${SEP:?}"; POS="${POS-}${POS+${SEP:?}}${*-}"; IFS="${_IFS?}"; break ;;
|
||
|
# If a long option in the form "--opt=value" is found, it is split into "--opt" and "value".
|
||
|
--*=*) optSplitEquals "${@-}"; shift; set -- "${optName:?}" "${optArg?}" "${@-}"; continue ;;
|
||
|
# If an option did not match any pattern, an error is thrown.
|
||
|
-?|--*) optDie "Illegal option ${1:?}" ;;
|
||
|
# If multiple short options in the form "-AB" are found, they are split into "-A" and "-B".
|
||
|
-?*) optSplitShort "${@-}"; shift; set -- "${optAName:?}" "${optBName:?}" "${@-}"; continue ;;
|
||
|
# If a positional parameter is found, it is saved.
|
||
|
*) POS="${POS-}${POS+${SEP:?}}${1?}" ;;
|
||
|
esac
|
||
|
shift
|
||
|
done
|
||
|
}
|
||
|
optSplitShort() {
|
||
|
optAName="${1%"${1#??}"}"; optBName="-${1#??}"
|
||
|
}
|
||
|
optSplitEquals() {
|
||
|
optName="${1%="${1#--*=}"}"; optArg="${1#--*=}"
|
||
|
}
|
||
|
optArgStr() {
|
||
|
if [ -n "${1#??}" ] && [ "${1#--}" = "${1:?}" ]; then optArg="${1#??}"; optShift='0';
|
||
|
elif [ -n "${2+x}" ]; then optArg="${2-}"; optShift='1';
|
||
|
else optDie "No argument for ${1:?} option"; fi
|
||
|
}
|
||
|
optArgBool() {
|
||
|
if [ "${1#--no-}" = "${1:?}" ]; then optArg='true';
|
||
|
else optArg='false'; fi
|
||
|
}
|
||
|
optDie() {
|
||
|
printf '%s\n' "${@-}" "Try 'hblock --help' for more information" >&2
|
||
|
exit 2
|
||
|
}
|
||
|
|
||
|
# Show help and quit.
|
||
|
showHelp() {
|
||
|
printf '%s\n' "$(sed -e 's/%NL/\n/g' <<-EOF
|
||
|
Usage: hblock [OPTION]...
|
||
|
|
||
|
hBlock is a POSIX-compliant shell script that gets a list of domains that serve
|
||
|
ads, tracking scripts and malware from multiple sources and creates a hosts
|
||
|
file, among other formats, that prevents your system from connecting to them.
|
||
|
|
||
|
Options:
|
||
|
|
||
|
-O, --output <FILE|->, \${HBLOCK_OUTPUT_FILE}%NL
|
||
|
Output file location.%NL
|
||
|
If equals "-", it is printed to stdout.%NL
|
||
|
(default: ${outputFile?})%NL
|
||
|
-H, --header <FILE|builtin|none|->, \${HBLOCK_HEADER_FILE}%NL
|
||
|
File to be included at the beginning of the output file.%NL
|
||
|
If equals "builtin", the built-in value is used.%NL
|
||
|
If equals "none", an empty value is used.%NL
|
||
|
If equals "-", the stdin content is used.%NL
|
||
|
If unspecified and any of the following files exists, its content is used.%NL
|
||
|
\${XDG_CONFIG_HOME}/hblock/header%NL
|
||
|
${ETCDIR?}/hblock/header%NL
|
||
|
(default: ${headerFile?})%NL
|
||
|
-F, --footer <FILE|builtin|none|->, \${HBLOCK_FOOTER_FILE}%NL
|
||
|
File to be included at the end of the output file.%NL
|
||
|
If equals "builtin", the built-in value is used.%NL
|
||
|
If equals "none", an empty value is used.%NL
|
||
|
If equals "-", the stdin content is used.%NL
|
||
|
If unspecified and any of the following files exists, its content is used.%NL
|
||
|
\${XDG_CONFIG_HOME}/hblock/footer%NL
|
||
|
${ETCDIR?}/hblock/footer%NL
|
||
|
(default: ${footerFile?})%NL
|
||
|
-S, --sources <FILE|builtin|none|->, \${HBLOCK_SOURCES_FILE}%NL
|
||
|
File with line separated URLs used to generate the blocklist.%NL
|
||
|
If equals "builtin", the built-in value is used.%NL
|
||
|
If equals "none", an empty value is used.%NL
|
||
|
If equals "-", the stdin content is used.%NL
|
||
|
If unspecified and any of the following files exists, its content is used.%NL
|
||
|
\${XDG_CONFIG_HOME}/hblock/sources.list%NL
|
||
|
${ETCDIR?}/hblock/sources.list%NL
|
||
|
(default: ${sourcesFile?})%NL
|
||
|
-A, --allowlist <FILE|builtin|none|->, \${HBLOCK_ALLOWLIST_FILE}%NL
|
||
|
File with line separated entries to be removed from the blocklist.%NL
|
||
|
If equals "builtin", the built-in value is used.%NL
|
||
|
If equals "none", an empty value is used.%NL
|
||
|
If equals "-", the stdin content is used.%NL
|
||
|
If unspecified and any of the following files exists, its content is used.%NL
|
||
|
\${XDG_CONFIG_HOME}/hblock/allow.list%NL
|
||
|
${ETCDIR?}/hblock/allow.list%NL
|
||
|
(default: ${allowlistFile?})%NL
|
||
|
-D, --denylist <FILE|builtin|none|->, \${HBLOCK_DENYLIST_FILE}%NL
|
||
|
File with line separated entries to be added to the blocklist.%NL
|
||
|
If equals "builtin", the built-in value is used.%NL
|
||
|
If equals "none", an empty value is used.%NL
|
||
|
If equals "-", the stdin content is used.%NL
|
||
|
If unspecified and any of the following files exists, its content is used.%NL
|
||
|
\${XDG_CONFIG_HOME}/hblock/deny.list%NL
|
||
|
${ETCDIR?}/hblock/deny.list%NL
|
||
|
(default: ${denylistFile?})%NL
|
||
|
-R, --redirection <REDIRECTION>, \${HBLOCK_REDIRECTION}%NL
|
||
|
Redirection for all entries in the blocklist.%NL
|
||
|
(default: ${redirection?})%NL
|
||
|
-W, --wrap <NUMBER>, \${HBLOCK_WRAP}%NL
|
||
|
Break blocklist lines after this number of entries.%NL
|
||
|
(default: ${wrap?})%NL
|
||
|
-T, --template <TEMPLATE>, \${HBLOCK_TEMPLATE}%NL
|
||
|
Template applied to each entry.%NL
|
||
|
%D = <DOMAIN>, %R = <REDIRECTION>%NL
|
||
|
(default: ${template?})%NL
|
||
|
-C, --comment <COMMENT>, \${HBLOCK_COMMENT}%NL
|
||
|
Character used for comments.%NL
|
||
|
(default: ${comment?})%NL
|
||
|
-l, --[no-]lenient, \${HBLOCK_LENIENT}%NL
|
||
|
Match all entries from sources regardless of their IP, instead of
|
||
|
0.0.0.0, 127.0.0.1, ::, ::1 or nothing.%NL
|
||
|
(default: ${lenient?})%NL
|
||
|
-r, --[no-]regex, \${HBLOCK_REGEX}%NL
|
||
|
Use POSIX BREs in the allowlist instead of fixed strings.%NL
|
||
|
(default: ${regex?})%NL
|
||
|
-f, --[no-]filter-subdomains, \${HBLOCK_FILTER_SUBDOMAINS}%NL
|
||
|
Do not include subdomains when the parent domain is also blocked.
|
||
|
Useful for reducing the blocklist size in cases such as when DNS blocking
|
||
|
makes these subdomains redundant.%NL
|
||
|
(default: ${filterSubdomains?})%NL
|
||
|
-c, --[no-]continue, \${HBLOCK_CONTINUE}%NL
|
||
|
Do not abort if a download error occurs.%NL
|
||
|
(default: ${continue?})%NL
|
||
|
-p, --parallel, \${HBLOCK_PARALLEL}%NL
|
||
|
Maximum concurrency for parallel downloads.%NL
|
||
|
(default: ${parallel?})%NL
|
||
|
-q, --[no-]quiet, \${HBLOCK_QUIET}%NL
|
||
|
Suppress non-error messages.%NL
|
||
|
(default: ${quiet?})%NL
|
||
|
-x, --color <auto|true|false>, \${HBLOCK_COLOR}%NL
|
||
|
Colorize the output.%NL
|
||
|
(default: ${color?})%NL
|
||
|
-v, --version%NL
|
||
|
Show version number and quit.%NL
|
||
|
-h, --help%NL
|
||
|
Show this help and quit.
|
||
|
|
||
|
Report bugs to: <https://github.com/hectorm/hblock/issues>
|
||
|
EOF
|
||
|
)"
|
||
|
exit 0
|
||
|
}
|
||
|
|
||
|
# Show version number and quit.
|
||
|
showVersion() {
|
||
|
printf '%s\n' "$(cat <<-EOF
|
||
|
hBlock ${HBLOCK_VERSION:?}
|
||
|
Author: ${HBLOCK_AUTHOR:?}
|
||
|
License: ${HBLOCK_LICENSE:?}
|
||
|
Repository: ${HBLOCK_REPOSITORY:?}
|
||
|
EOF
|
||
|
)"
|
||
|
exit 0
|
||
|
}
|
||
|
|
||
|
# Check if a program exists.
|
||
|
exists() {
|
||
|
# shellcheck disable=SC2230
|
||
|
if command -v true; then command -v -- "${1:?}"
|
||
|
elif eval type type; then eval type -- "${1:?}"
|
||
|
else which -- "${1:?}"; fi >/dev/null 2>&1
|
||
|
}
|
||
|
|
||
|
# Pretty print methods.
|
||
|
printInfo() { [ -n "${NO_STDOUT+x}" ] || printf "${COLOR_RESET-}[${COLOR_BGREEN-}INFO${COLOR_RESET-}] %s\n" "${@-}"; }
|
||
|
printWarn() { [ -n "${NO_STDERR+x}" ] || printf "${COLOR_RESET-}[${COLOR_BYELLOW-}WARN${COLOR_RESET-}] %s\n" "${@-}" >&2; }
|
||
|
printError() { [ -n "${NO_STDERR+x}" ] || printf "${COLOR_RESET-}[${COLOR_BRED-}ERROR${COLOR_RESET-}] %s\n" "${@-}" >&2; }
|
||
|
printList() { [ -n "${NO_STDOUT+x}" ] || printf "${COLOR_RESET-} ${COLOR_BCYAN-}*${COLOR_RESET-} %s\n" "${@-}"; }
|
||
|
|
||
|
# Print a pseudorandom string.
|
||
|
rand() { :& awk -v N="${!}" 'BEGIN{srand();printf("%08x%06x",rand()*2^31-1,N)}'; }
|
||
|
|
||
|
# Create a temporary directory, file or FIFO special file.
|
||
|
createTemp() {
|
||
|
# POSIX does not specify the mktemp utility, so here comes a hacky solution.
|
||
|
while t="${TMPDIR:-${TMP:-/tmp}}/hblock.${$}.$(rand)" && [ -e "${t:?}" ]; do sleep 1; done
|
||
|
(
|
||
|
umask 077
|
||
|
case "${1-}" in
|
||
|
'dir') mkdir -- "${t:?}" ;;
|
||
|
'file') touch -- "${t:?}" ;;
|
||
|
'fifo') mkfifo -- "${t:?}" ;;
|
||
|
esac
|
||
|
printf '%s' "${t:?}"
|
||
|
)
|
||
|
}
|
||
|
|
||
|
# Write stdin to a file.
|
||
|
sponge() {
|
||
|
spongeFile="$(createTemp 'file')"; cat > "${spongeFile:?}"
|
||
|
cat -- "${spongeFile:?}" > "${1:?}"; rm -f -- "${spongeFile:?}"
|
||
|
}
|
||
|
|
||
|
# Count files or directories in a directory.
|
||
|
dirCount() { [ -e "${1:?}" ] && printf '%s' "${#}" || printf '%s' '0'; }
|
||
|
|
||
|
# Print to stdout the contents of a URL.
|
||
|
fetchUrl() {
|
||
|
# If the protocol is "file://" we can omit the download and simply use cat.
|
||
|
if [ "${1#file://}" != "${1:?}" ]; then cat -- "${1#file://}"
|
||
|
else
|
||
|
userAgent='Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0'
|
||
|
if exists curl; then curl -fsSL -A "${userAgent:?}" -- "${1:?}"
|
||
|
elif exists wget; then wget -qO- -U "${userAgent:?}" -- "${1:?}"
|
||
|
elif exists fetch; then fetch -qo- --user-agent="${userAgent:?}" -- "${1:?}"
|
||
|
else
|
||
|
printError 'curl, wget or fetch are required for this script'
|
||
|
exit 1
|
||
|
fi
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
# Remove comments from string.
|
||
|
removeComments() { sed -e 's/[[:blank:]]*#.*//;/^$/d'; }
|
||
|
|
||
|
# Transform hosts file entries to domain names.
|
||
|
sanitizeBlocklist() {
|
||
|
leadingScript='s/^[[:blank:]]*//'
|
||
|
trailingScript='s/[[:blank:]]*\(#.*\)\{0,1\}$//'
|
||
|
if [ "${1:?}" = 'true' ]; then
|
||
|
ipv4Script='s/^\([0-9]\{1,3\}\.\)\{3\}[0-9]\{1,3\}[[:blank:]]\{1,\}//'
|
||
|
ipv6Script='s/^\([0-9a-f]\{0,4\}:\)\{2,7\}[0-9a-f]\{0,4\}[[:blank:]]\{1,\}//'
|
||
|
else
|
||
|
ipv4Script='s/^\(0\)\{0,1\}\(127\)\{0,1\}\(\.[0-9]\{1,3\}\)\{3\}[[:blank:]]\{1,\}//'
|
||
|
ipv6Script='s/^\(0\{0,4\}:\)\{2,7\}0\{0,3\}[01]\{0,1\}[[:blank:]]\{1,\}//'
|
||
|
fi
|
||
|
domainRegex='\([0-9a-z_-]\{1,63\}\.\)\{1,\}[a-z][0-9a-z-]\{0,61\}[0-9a-z]\.\{0,1\}'
|
||
|
tr -d '\r' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' \
|
||
|
| sed -e "${leadingScript:?};${ipv4Script:?};${ipv6Script:?};${trailingScript:?}" \
|
||
|
| { grep -e "^${domainRegex:?}\([[:blank:]]\{1,\}${domainRegex:?}\)*$" ||:; } \
|
||
|
| tr -s ' \t' '\n' | sed 's/\.$//'
|
||
|
}
|
||
|
|
||
|
# Remove reserved Top Level Domains.
|
||
|
removeReservedTLDs() {
|
||
|
sed -e '/\.corp$/d' \
|
||
|
-e '/\.domain$/d' \
|
||
|
-e '/\.example$/d' \
|
||
|
-e '/\.home$/d' \
|
||
|
-e '/\.host$/d' \
|
||
|
-e '/\.invalid$/d' \
|
||
|
-e '/\.lan$/d' \
|
||
|
-e '/\.local$/d' \
|
||
|
-e '/\.localdomain$/d' \
|
||
|
-e '/\.localhost$/d' \
|
||
|
-e '/\.test$/d'
|
||
|
}
|
||
|
|
||
|
main() {
|
||
|
usrConfDir="${XDG_CONFIG_HOME?}/hblock"
|
||
|
sysConfDir="${ETCDIR?}/hblock"
|
||
|
|
||
|
# Source environment file if exists.
|
||
|
# shellcheck disable=SC1091
|
||
|
if [ -f "${usrConfDir:?}/environment" ]; then
|
||
|
set -a; . "${usrConfDir:?}/environment"; set +a
|
||
|
elif [ -f "${sysConfDir:?}/environment" ]; then
|
||
|
set -a; . "${sysConfDir:?}/environment"; set +a
|
||
|
fi
|
||
|
|
||
|
# Output file location.
|
||
|
outputFile="${HBLOCK_OUTPUT_FILE-"${ETCDIR?}/hosts"}"
|
||
|
|
||
|
# File to be included at the beginning of the output file.
|
||
|
headerFile='builtin'
|
||
|
if [ -n "${HBLOCK_HEADER+x}" ]; then
|
||
|
HBLOCK_HEADER_BUILTIN="${HBLOCK_HEADER?}"
|
||
|
elif [ -n "${HBLOCK_HEADER_FILE+x}" ]; then
|
||
|
headerFile="${HBLOCK_HEADER_FILE?}"
|
||
|
elif [ -f "${usrConfDir:?}/header" ]; then
|
||
|
headerFile="${usrConfDir:?}/header"
|
||
|
elif [ -f "${sysConfDir:?}/header" ]; then
|
||
|
headerFile="${sysConfDir:?}/header"
|
||
|
fi
|
||
|
|
||
|
# File to be included at the end of the output file.
|
||
|
footerFile='builtin'
|
||
|
if [ -n "${HBLOCK_FOOTER+x}" ]; then
|
||
|
HBLOCK_FOOTER_BUILTIN="${HBLOCK_FOOTER?}"
|
||
|
elif [ -n "${HBLOCK_FOOTER_FILE+x}" ]; then
|
||
|
footerFile="${HBLOCK_FOOTER_FILE?}"
|
||
|
elif [ -f "${usrConfDir:?}/footer" ]; then
|
||
|
footerFile="${usrConfDir:?}/footer"
|
||
|
elif [ -f "${sysConfDir:?}/footer" ]; then
|
||
|
footerFile="${sysConfDir:?}/footer"
|
||
|
fi
|
||
|
|
||
|
# File with line separated URLs used to generate the blocklist.
|
||
|
sourcesFile='builtin'
|
||
|
if [ -n "${HBLOCK_SOURCES+x}" ]; then
|
||
|
HBLOCK_SOURCES_BUILTIN="${HBLOCK_SOURCES?}"
|
||
|
elif [ -n "${HBLOCK_SOURCES_FILE+x}" ]; then
|
||
|
sourcesFile="${HBLOCK_SOURCES_FILE?}"
|
||
|
elif [ -f "${usrConfDir:?}/sources.list" ]; then
|
||
|
sourcesFile="${usrConfDir:?}/sources.list"
|
||
|
elif [ -f "${sysConfDir:?}/sources.list" ]; then
|
||
|
sourcesFile="${sysConfDir:?}/sources.list"
|
||
|
fi
|
||
|
|
||
|
# File with line separated entries to be removed from the blocklist.
|
||
|
allowlistFile='builtin'
|
||
|
if [ -n "${HBLOCK_ALLOWLIST+x}" ]; then
|
||
|
HBLOCK_ALLOWLIST_BUILTIN="${HBLOCK_ALLOWLIST?}"
|
||
|
elif [ -n "${HBLOCK_ALLOWLIST_FILE+x}" ]; then
|
||
|
allowlistFile="${HBLOCK_ALLOWLIST_FILE?}"
|
||
|
elif [ -f "${usrConfDir:?}/allow.list" ]; then
|
||
|
allowlistFile="${usrConfDir:?}/allow.list"
|
||
|
elif [ -f "${sysConfDir:?}/allow.list" ]; then
|
||
|
allowlistFile="${sysConfDir:?}/allow.list"
|
||
|
fi
|
||
|
|
||
|
# File with line separated entries to be added to the blocklist.
|
||
|
denylistFile='builtin'
|
||
|
if [ -n "${HBLOCK_DENYLIST+x}" ]; then
|
||
|
HBLOCK_DENYLIST_BUILTIN="${HBLOCK_DENYLIST?}"
|
||
|
elif [ -n "${HBLOCK_DENYLIST_FILE+x}" ]; then
|
||
|
denylistFile="${HBLOCK_DENYLIST_FILE?}"
|
||
|
elif [ -f "${usrConfDir:?}/deny.list" ]; then
|
||
|
denylistFile="${usrConfDir:?}/deny.list"
|
||
|
elif [ -f "${sysConfDir:?}/deny.list" ]; then
|
||
|
denylistFile="${sysConfDir:?}/deny.list"
|
||
|
fi
|
||
|
|
||
|
# Redirection for all entries in the blocklist.
|
||
|
redirection="${HBLOCK_REDIRECTION-"0.0.0.0"}"
|
||
|
|
||
|
# Break blocklist lines after this number of entries.
|
||
|
wrap="${HBLOCK_WRAP-"1"}"
|
||
|
|
||
|
# Template applied to each entry.
|
||
|
template="${HBLOCK_TEMPLATE-"%R %D"}"
|
||
|
|
||
|
# Character used for comments.
|
||
|
comment="${HBLOCK_COMMENT-"#"}"
|
||
|
|
||
|
# Match all entries from sources, regardless of their IP.
|
||
|
lenient="${HBLOCK_LENIENT-"false"}"
|
||
|
|
||
|
# Use POSIX BREs instead of fixed strings.
|
||
|
regex="${HBLOCK_REGEX-"false"}"
|
||
|
|
||
|
# Do not include subdomains when the parent domain is also blocked.
|
||
|
filterSubdomains="${HBLOCK_FILTER_SUBDOMAINS-"false"}"
|
||
|
|
||
|
# Abort if a download error occurs.
|
||
|
continue="${HBLOCK_CONTINUE-"false"}"
|
||
|
|
||
|
# Maximum concurrency for parallel downloads.
|
||
|
parallel="${HBLOCK_PARALLEL-"4"}"
|
||
|
|
||
|
# Colorize the output.
|
||
|
color="${HBLOCK_COLOR-"auto"}"
|
||
|
|
||
|
# Suppress non-error messages.
|
||
|
quiet="${HBLOCK_QUIET-"false"}"
|
||
|
|
||
|
# Parse command line options.
|
||
|
# shellcheck disable=SC2086
|
||
|
{ optParse "${@-}"; _IFS="${IFS?}"; IFS="${SEP:?}"; set -- ${POS-} >/dev/null; IFS="${_IFS?}"; }
|
||
|
|
||
|
# Define terminal colors if the color option is enabled or in auto mode if STDOUT is attached to a TTY and the
|
||
|
# "NO_COLOR" variable is not set (https://no-color.org).
|
||
|
if [ "${color:?}" = 'true' ] || { [ "${color:?}" = 'auto' ] && [ -z "${NO_COLOR+x}" ] && [ -t 1 ]; }; then
|
||
|
COLOR_RESET="$({ exists tput && tput sgr0; } 2>/dev/null || printf '\033[0m')"
|
||
|
COLOR_BRED="$({ exists tput && tput bold && tput setaf 1; } 2>/dev/null || printf '\033[1;31m')"
|
||
|
COLOR_BGREEN="$({ exists tput && tput bold && tput setaf 2; } 2>/dev/null || printf '\033[1;32m')"
|
||
|
COLOR_BYELLOW="$({ exists tput && tput bold && tput setaf 3; } 2>/dev/null || printf '\033[1;33m')"
|
||
|
COLOR_BCYAN="$({ exists tput && tput bold && tput setaf 6; } 2>/dev/null || printf '\033[1;36m')"
|
||
|
fi
|
||
|
|
||
|
# Set "NO_STDOUT" variable if the quiet option is enabled (other methods will honor this variable).
|
||
|
if [ "${quiet:?}" = 'true' ]; then
|
||
|
NO_STDOUT='true'
|
||
|
fi
|
||
|
|
||
|
# Check the header file.
|
||
|
case "${headerFile:?}" in
|
||
|
# If the file value equals "-", use stdin.
|
||
|
'-') headerFile="$(createTemp 'file')"; cat <&0 > "${headerFile:?}" ;;
|
||
|
# If the file value equals "none", use an empty file.
|
||
|
'none') headerFile="$(createTemp 'file')" ;;
|
||
|
# If the file value equals "builtin", use the built-in value.
|
||
|
'builtin') headerFile="$(createTemp 'file')"; printf '%s' "${HBLOCK_HEADER_BUILTIN?}" > "${headerFile:?}" ;;
|
||
|
# If the file does not exist, throw an error.
|
||
|
*) [ -e "${headerFile:?}" ] || { printError "No such file: ${headerFile:?}"; exit 1; } ;;
|
||
|
esac
|
||
|
|
||
|
# Check the footer file.
|
||
|
case "${footerFile:?}" in
|
||
|
# If the file value equals "-", use stdin.
|
||
|
'-') footerFile="$(createTemp 'file')"; cat <&0 > "${footerFile:?}" ;;
|
||
|
# If the file value equals "none", use an empty file.
|
||
|
'none') footerFile="$(createTemp 'file')" ;;
|
||
|
# If the file value equals "builtin", use the built-in value.
|
||
|
'builtin') footerFile="$(createTemp 'file')"; printf '%s' "${HBLOCK_FOOTER_BUILTIN?}" > "${footerFile:?}" ;;
|
||
|
# If the file does not exist, throw an error.
|
||
|
*) [ -e "${footerFile:?}" ] || { printError "No such file: ${footerFile:?}"; exit 1; } ;;
|
||
|
esac
|
||
|
|
||
|
# Check the sources file.
|
||
|
case "${sourcesFile:?}" in
|
||
|
# If the file value equals "-", use stdin.
|
||
|
'-') sourcesFile="$(createTemp 'file')"; cat <&0 > "${sourcesFile:?}" ;;
|
||
|
# If the file value equals "none", use an empty file.
|
||
|
'none') sourcesFile="$(createTemp 'file')" ;;
|
||
|
# If the file value equals "builtin", use the built-in value.
|
||
|
'builtin') sourcesFile="$(createTemp 'file')"; printf '%s' "${HBLOCK_SOURCES_BUILTIN?}" > "${sourcesFile:?}" ;;
|
||
|
# If the file does not exist, throw an error.
|
||
|
*) [ -e "${sourcesFile:?}" ] || { printError "No such file: ${sourcesFile:?}"; exit 1; } ;;
|
||
|
esac
|
||
|
|
||
|
# Check the allowlist file.
|
||
|
case "${allowlistFile:?}" in
|
||
|
# If the file value equals "-", use stdin.
|
||
|
'-') allowlistFile="$(createTemp 'file')"; cat <&0 > "${allowlistFile:?}" ;;
|
||
|
# If the file value equals "none", use an empty file.
|
||
|
'none') allowlistFile="$(createTemp 'file')" ;;
|
||
|
# If the file value equals "builtin", use the built-in value.
|
||
|
'builtin') allowlistFile="$(createTemp 'file')"; printf '%s' "${HBLOCK_ALLOWLIST_BUILTIN?}" > "${allowlistFile:?}" ;;
|
||
|
# If the file does not exist, throw an error.
|
||
|
*) [ -e "${allowlistFile:?}" ] || { printError "No such file: ${allowlistFile:?}"; exit 1; } ;;
|
||
|
esac
|
||
|
|
||
|
# Check the denylist file.
|
||
|
case "${denylistFile:?}" in
|
||
|
# If the file value equals "-", use stdin.
|
||
|
'-') denylistFile="$(createTemp 'file')"; cat <&0 > "${denylistFile:?}" ;;
|
||
|
# If the file value equals "none", use an empty file.
|
||
|
'none') denylistFile="$(createTemp 'file')" ;;
|
||
|
# If the file value equals "builtin", use the built-in value.
|
||
|
'builtin') denylistFile="$(createTemp 'file')"; printf '%s' "${HBLOCK_DENYLIST_BUILTIN?}" > "${denylistFile:?}" ;;
|
||
|
# If the file does not exist, throw an error.
|
||
|
*) [ -e "${denylistFile:?}" ] || { printError "No such file: ${denylistFile:?}"; exit 1; } ;;
|
||
|
esac
|
||
|
|
||
|
# Create an empty blocklist file.
|
||
|
blocklistFile="$(createTemp 'file')"
|
||
|
|
||
|
# If the sources file is not empty, each source is downloaded and appended to the blocklist file.
|
||
|
if [ -s "${sourcesFile:?}" ]; then
|
||
|
printInfo 'Downloading sources'
|
||
|
|
||
|
sourcesDlDir="$(createTemp 'dir')"
|
||
|
sourcesUrlFile="$(createTemp 'file')"
|
||
|
|
||
|
# Read the sources file ignoring comments or empty lines.
|
||
|
removeComments < "${sourcesFile:?}" > "${sourcesUrlFile:?}"
|
||
|
|
||
|
while IFS= read -r url || [ -n "${url?}" ]; do
|
||
|
# Wait if the number of running jobs exceeds the concurrency limit.
|
||
|
if [ "${parallel:?}" -gt '0' ]; then
|
||
|
while [ "${parallel:?}" -le "$(dirCount "${sourcesDlDir:?}"/*.part)" ]; do
|
||
|
# POSIX does not specify the "-n" option, wait for the last PID as fallback.
|
||
|
# shellcheck disable=SC3045
|
||
|
wait -n 2>/dev/null || wait "${!}"
|
||
|
done
|
||
|
fi
|
||
|
|
||
|
# Initialize the download job and send it to the background.
|
||
|
printList "${url:?}"
|
||
|
sourceDlFile="${sourcesDlDir:?}"/"$(rand)"
|
||
|
touch -- "${sourceDlFile:?}.part"
|
||
|
{
|
||
|
if fetchUrl "${url:?}" > "${sourceDlFile:?}.part"; then
|
||
|
if [ -e "${sourceDlFile:?}.part" ]; then
|
||
|
printf '\n' >> "${sourceDlFile:?}.part"
|
||
|
mv -- "${sourceDlFile:?}.part" "${sourceDlFile:?}"
|
||
|
fi
|
||
|
else
|
||
|
rm -f -- "${sourceDlFile:?}.part"
|
||
|
if [ "${continue:?}" = 'true' ]; then
|
||
|
printWarn "Cannot obtain source: ${url:?}"
|
||
|
else
|
||
|
printError "Cannot obtain source: ${url:?}"
|
||
|
{ kill "${$}"; exit 1; } 2>/dev/null
|
||
|
fi
|
||
|
fi
|
||
|
} &
|
||
|
done < "${sourcesUrlFile:?}"
|
||
|
wait
|
||
|
|
||
|
# Append downloaded sources to the blocklist file.
|
||
|
cat -- "${sourcesDlDir:?}"/* >> "${blocklistFile:?}"
|
||
|
rm -rf -- "${sourcesDlDir:?}"
|
||
|
fi
|
||
|
|
||
|
# If the denylist file is not empty, it is appended to the blocklist file.
|
||
|
if [ -s "${denylistFile:?}" ]; then
|
||
|
printInfo 'Applying denylist'
|
||
|
cat -- "${denylistFile:?}" >> "${blocklistFile:?}"
|
||
|
fi
|
||
|
|
||
|
# If the blocklist file is not empty, it is sanitized.
|
||
|
if [ -s "${blocklistFile:?}" ]; then
|
||
|
printInfo 'Sanitizing blocklist'
|
||
|
sanitizeBlocklist "${lenient:?}" < "${blocklistFile:?}" | removeReservedTLDs | sponge "${blocklistFile:?}"
|
||
|
fi
|
||
|
|
||
|
# If the allowlist file is not empty, the entries on it are removed from the blocklist file.
|
||
|
if [ -s "${allowlistFile:?}" ]; then
|
||
|
printInfo 'Applying allowlist'
|
||
|
allowlistPatternFile="$(createTemp 'file')"
|
||
|
# Entries are treated as regexes depending on whether the regex option is enabled.
|
||
|
removeComments < "${allowlistFile:?}" >> "${allowlistPatternFile:?}"
|
||
|
if [ "${regex:?}" = 'true' ]; then
|
||
|
grep -vf "${allowlistPatternFile:?}" -- "${blocklistFile:?}" | sponge "${blocklistFile:?}"
|
||
|
else
|
||
|
grep -Fxvf "${allowlistPatternFile:?}" -- "${blocklistFile:?}" | sponge "${blocklistFile:?}"
|
||
|
fi
|
||
|
rm -f -- "${allowlistPatternFile:?}"
|
||
|
fi
|
||
|
|
||
|
# If the blocklist file is not empty, it is filtered and sorted.
|
||
|
if [ -s "${blocklistFile:?}" ]; then
|
||
|
if [ "${filterSubdomains:?}" = 'true' ]; then
|
||
|
printInfo 'Filtering redundant subdomains'
|
||
|
awkReverseScript="$(cat <<-'EOF'
|
||
|
BEGIN { FS = "." }
|
||
|
{
|
||
|
for (i = NF; i > 0; i--) {
|
||
|
printf("%s%s", $i, (i > 1 ? FS : RS))
|
||
|
}
|
||
|
}
|
||
|
EOF
|
||
|
)"
|
||
|
awkFilterScript="$(cat <<-'EOF'
|
||
|
BEGIN { p = "." }
|
||
|
{
|
||
|
if (index($0, p) != 1) {
|
||
|
print($0); p = $0"."
|
||
|
}
|
||
|
}
|
||
|
EOF
|
||
|
)"
|
||
|
awk "${awkReverseScript:?}" < "${blocklistFile:?}" | sort \
|
||
|
| awk "${awkFilterScript:?}" | awk "${awkReverseScript:?}" \
|
||
|
| sponge "${blocklistFile:?}"
|
||
|
fi
|
||
|
|
||
|
printInfo 'Sorting blocklist'
|
||
|
sort < "${blocklistFile:?}" | uniq | sponge "${blocklistFile:?}"
|
||
|
fi
|
||
|
|
||
|
# Count blocked domains.
|
||
|
blocklistCount="$(wc -l < "${blocklistFile:?}" | awk '{print($1)}')"
|
||
|
|
||
|
# If the blocklist file is not empty, the format template is applied.
|
||
|
if [ -s "${blocklistFile:?}" ]; then
|
||
|
printInfo 'Applying format template'
|
||
|
# The number of domains per line is equal to the value of the wrap option.
|
||
|
if [ "${wrap:?}" -gt '1' ]; then
|
||
|
awkWrapScript='{ORS=(NR%W?FS:RS)}1;END{if(NR%W){printf(RS)}}'
|
||
|
awk -v FS=' ' -v RS='\n' -v W="${wrap:?}" "${awkWrapScript:?}" < "${blocklistFile:?}" \
|
||
|
| sponge "${blocklistFile:?}"
|
||
|
fi
|
||
|
# The following awk script replaces in the template the variables starting with a % sign with their value.
|
||
|
awkTemplateScript="$(cat <<-'EOF'
|
||
|
BEGIN {
|
||
|
Tl = length(T); split(T, Ta, "")
|
||
|
for (i = 1; i <= Tl; i++) {
|
||
|
if (Ta[i] == "%") {
|
||
|
i++; if (Ta[i] == "D") { Vn[++Vl] = "D"; Vp[Vl] = i - 1 }
|
||
|
else if (Ta[i] == "R") { Vn[++Vl] = "R"; Vp[Vl] = i - 1 }
|
||
|
else if (Ta[i] == "%") { Vn[++Vl] = "%"; Vp[Vl] = i - 1 }
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
{
|
||
|
o = T
|
||
|
for (i = Vl; i > 0 ; i--) {
|
||
|
if (Vn[i] == "D") v = $0
|
||
|
else if (Vn[i] == "R") v = R
|
||
|
else if (Vn[i] == "%") v = "%"
|
||
|
else v = ""
|
||
|
o = substr(o, 1, Vp[i] - 1) v substr(o, Vp[i] + 2)
|
||
|
}
|
||
|
print(o)
|
||
|
}
|
||
|
EOF
|
||
|
)"
|
||
|
awk -v T="${template?}" -v R="${redirection?}" "${awkTemplateScript:?}" < "${blocklistFile:?}" \
|
||
|
| sponge "${blocklistFile:?}"
|
||
|
fi
|
||
|
|
||
|
printOutputFile() {
|
||
|
# Define "C" variable for convenience.
|
||
|
C="${comment?}"
|
||
|
|
||
|
# Append banner to the output file.
|
||
|
if [ -n "${C?}" ]; then
|
||
|
cat <<-EOF
|
||
|
${C?} Generated with hBlock ${HBLOCK_VERSION:?} (${HBLOCK_REPOSITORY:?})
|
||
|
${C?} Blocked domains: ${blocklistCount:?}
|
||
|
EOF
|
||
|
if [ -z "${SOURCE_DATE_EPOCH+x}" ]; then
|
||
|
cat <<-EOF
|
||
|
${C?} Date: $(date)
|
||
|
EOF
|
||
|
fi
|
||
|
fi
|
||
|
|
||
|
# If the header file is not empty, it is appended to the output file.
|
||
|
if [ -s "${headerFile:?}" ]; then
|
||
|
[ -z "${C?}" ] || printf '\n%s\n' "${C?} BEGIN HEADER"
|
||
|
awk 1 < "${headerFile:?}"
|
||
|
[ -z "${C?}" ] || printf '%s\n' "${C?} END HEADER"
|
||
|
fi
|
||
|
|
||
|
# If the blocklist file is not empty, it is appended to the output file.
|
||
|
if [ -s "${blocklistFile:?}" ]; then
|
||
|
[ -z "${C?}" ] || printf '\n%s\n' "${C?} BEGIN BLOCKLIST"
|
||
|
awk 1 < "${blocklistFile:?}"
|
||
|
[ -z "${C?}" ] || printf '%s\n' "${C?} END BLOCKLIST"
|
||
|
fi
|
||
|
|
||
|
# If the footer file is not empty, it is appended to the output file.
|
||
|
if [ -s "${footerFile:?}" ]; then
|
||
|
[ -z "${C?}" ] || printf '\n%s\n' "${C?} BEGIN FOOTER"
|
||
|
awk 1 < "${footerFile:?}"
|
||
|
[ -z "${C?}" ] || printf '%s\n' "${C?} END FOOTER"
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
# If the file name equals "-", print to stdout.
|
||
|
if [ "${outputFile:?}" = '-' ]; then
|
||
|
printOutputFile
|
||
|
# Try writing the file.
|
||
|
elif touch -- "${outputFile:?}" >/dev/null 2>&1; then
|
||
|
printOutputFile > "${outputFile:?}"
|
||
|
# If writing fails, try with sudo.
|
||
|
elif exists sudo && exists tee; then
|
||
|
printOutputFile | sudo tee -- "${outputFile:?}" >/dev/null
|
||
|
# Throw an error for everything else.
|
||
|
else
|
||
|
printError "Cannot write file: ${outputFile:?}"
|
||
|
exit 1
|
||
|
fi
|
||
|
|
||
|
printInfo "${blocklistCount:?} blocked domains!"
|
||
|
}
|
||
|
|
||
|
main "${@-}"
|