#!/bin/sh # Version: 1.4.1 # Author: Héctor Molinero Fernández # Repository: https://github.com/zant95/hblock # License: MIT, https://opensource.org/licenses/MIT set -eu export LC_ALL=C # Default options output='/etc/hosts' redirection='0.0.0.0' backup=false backupDir='' lenient=false ignoreDownloadError=false color=auto quiet=false # shellcheck disable=SC2039 hostname=${HOSTNAME-$(uname -n)} # Default header block header=$(cat <<-EOF 127.0.0.1 localhost $hostname 255.255.255.255 broadcasthost ::1 localhost 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 ) # Default custom block custom='' # Default blocklist block blocklist='' # Default sources sources=$(cat <<-'EOF' https://raw.githubusercontent.com/zant95/hmirror/master/data/adaway.org/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/adblock-nocoin-list/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/adguard-simplified/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/disconnect.me-ad/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/disconnect.me-malvertising/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/disconnect.me-malware/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/disconnect.me-tracking/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/eth-phishing-detect/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/fademind-add.2o7net/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/fademind-add.dead/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/fademind-add.risk/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/fademind-add.spam/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/kadhosts/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/malwaredomainlist.com/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/malwaredomains.com-immortaldomains/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/malwaredomains.com-justdomains/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/matomo.org-spammers/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/mitchellkrogza-badd-boyz-hosts/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/pgl.yoyo.org/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/ransomwaretracker.abuse.ch/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/someonewhocares.org/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/spam404.com/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/stevenblack/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/winhelp2002.mvps.org/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/zerodot1-coinblockerlists-browser/list.txt https://raw.githubusercontent.com/zant95/hmirror/master/data/zeustracker.abuse.ch/list.txt EOF ) # Default whitelist (POSIX basic regex) # Examples: # \.com$ -> all domains that ends with '.com'. # ^example -> all domains that starts with 'example'. # ^sub\.example\.org$ -> domain 'sub.example.org'. whitelist=$(cat <<-'EOF' EOF ) # Default blacklist blacklist=$(cat <<-'EOF' EOF ) # Methods printStdout() { if [ "$quiet" != true ]; then # shellcheck disable=SC2059 printf -- "$@" fi } printStderr() { # shellcheck disable=SC2059 >&2 printf -- "$@" } logInfo() { printStdout ' - %s\n' "$@" } logAction() { if [ "$color" = true ]; then printStdout '\033[1;33m + \033[1;32m%s \033[0m\n' "$@" else printStdout ' + %s \n' "$@" fi } logError() { if [ "$color" = true ]; then printStderr '\033[1;33m + \033[1;31m%s \033[0m\n' "$@" else printStderr ' + %s \n' "$@" fi } getBoolVal() { [ "$1" = true ] && s='yes' || s='no' printf -- '%s' "$s" } checkBinary() { command -v -- "$@" >/dev/null 2>&1 } fetchUrl() { userAgent='Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0' if checkBinary curl; then curl -fsSL -A "$userAgent" -- "$@" else wget -qO- -U "$userAgent" -- "$@" fi } writeFile() { if [ -d "$2" ]; then logError "Cannot write '$2': is a directory" exit 1 elif ([ -e "$2" ] && [ -w "$2" ]) || touch -- "$2" >/dev/null 2>&1; then printf -- '%s\n' "$1" | tee -- "$2" >/dev/null elif checkBinary sudo; then printf -- '%s\n' "$1" | sudo tee -- "$2" >/dev/null else logError "Cannot write '$2': permission denied" exit 1 fi } showHelp() { if [ $# -eq 0 ]; then printStdout '%s\n' "$(cat <<-'EOF' Usage: hblock [options...] -O, --output FILE Hosts file location (default: /etc/hosts) -R, --redirection IP Destination IP for all entries in the blocklist (default: 0.0.0.0) -H, --header HEADER Content to be included at the beginning of the hosts file. You can use the output of any other command (e.g. "$(cat header.txt)") -S, --sources URLS Sources to be used to generate the blocklist (whitespace separated URLs) -W, --whitelist ENTRIES Entries to be removed from the blocklist (whitespace separated POSIX BREs) -B, --blacklist ENTRIES Entries to be added to the blocklist (whitespace separated domain names) -b, --backup [DIRECTORY] Make a time-stamped backup in DIRECTORY (default: output file directory) -l, --lenient Match any IP address from sources, although it will be replaced by the destination IP (default: 0.0.0.0, 127.0.0.1 or none) -i, --ignore-download-error Do not abort if a download error occurs -c, --color auto|true|false Colorize the output (default: auto) -q, --quiet Suppress non-error messages -v, --version Show version number and quit -h, --help Show this help and quit EOF )" exit 0 else [ -n "$1" ] && printStderr '%s\n' "Illegal option $1" printStderr '%s\n' "Try 'hblock --help' for more information" exit 1 fi } showVersion() { printStdout '%s\n' '1.4.1' exit 0 } main() { # Transform long options to short ones for opt in "$@"; do shift case "$opt" in '--output') set -- "$@" '-O' ;; '--redirection') set -- "$@" '-R' ;; '--header') set -- "$@" '-H' ;; '--sources') set -- "$@" '-S' ;; '--whitelist') set -- "$@" '-W' ;; '--blacklist') set -- "$@" '-B' ;; '--backup') set -- "$@" '-b' ;; '--lenient') set -- "$@" '-l' ;; '--ignore-download-error') set -- "$@" '-i' ;; '--color') set -- "$@" '-c' ;; '--quiet') set -- "$@" '-q' ;; '--version') set -- "$@" '-v' ;; '--help') set -- "$@" '-h' ;; *) set -- "$@" "$opt" esac done # Set omitted arguments to empty strings for opt in "$@"; do shift case "$opt" in -*b) if a="$*"; [ -z "$a" ] || [ "${a#\-}x" != "${a}x" ] then set -- "$@" "$opt" '' else set -- "$@" "$opt" fi ;; *) set -- "$@" "$opt" esac done # Read options OPTIND=1 while getopts ':O:R:H:S:W:B:b:lic:qvh-:' opt; do case "$opt" in 'O') output="$OPTARG" ;; 'R') redirection="$OPTARG" ;; 'H') header="$OPTARG" ;; 'S') sources="$OPTARG" ;; 'W') whitelist="$OPTARG" ;; 'B') blacklist="$OPTARG" ;; 'b') backup=true; backupDir="$OPTARG" ;; 'l') lenient=true ;; 'i') ignoreDownloadError=true ;; 'c') color="$OPTARG" ;; 'q') quiet=true ;; 'v') showVersion ;; 'h') showHelp ;; '-') showHelp "--$OPTARG" ;; *) showHelp "-$OPTARG" esac done # Check color support if [ "$color" = auto ]; then [ -t 1 ] && color=true || color=false fi logAction 'Configuration:' logInfo "Hosts file: $output" logInfo "Backup: $(getBoolVal "$backup")" logInfo "Destination IP: $redirection" logInfo "Lenient: $(getBoolVal "$lenient")" logInfo "Ignore download error: $(getBoolVal "$ignoreDownloadError")" logAction 'Downloading lists...' if ! checkBinary curl && ! checkBinary wget; then logError 'Either wget or curl are required for this script' exit 1 fi for url in $sources; do logInfo "$url" content=$(fetchUrl "$url") || true if [ -z "$content" ] && [ "$ignoreDownloadError" != true ]; then logError 'Download failed' exit 1 fi blocklist=$(printf -- '%s\n%s' "$blocklist" "$content") unset content done logAction 'Parsing lists...' if [ -n "$blocklist" ]; then logInfo 'Remove carriage return' blocklist=$(printf -- '%s' "$blocklist" | tr -d '\r') logInfo 'Transform to lowercase' blocklist=$(printf -- '%s' "$blocklist" | tr '[:upper:]' '[:lower:]') logInfo 'Remove comments' blocklist=$(printf -- '%s' "$blocklist" | sed 's/#.*//') logInfo 'Trim spaces' blocklist=$(printf -- '%s' "$blocklist" | sed \ -e 's/^[[:blank:]]*//' \ -e 's/[[:blank:]]*$//' ) logInfo 'Match hosts lines' if [ "$lenient" = true ]; then # This regex would be ideal, but it is not POSIX-compliant. # ipOctetRegex='\(25[0-5]\|2[0-4][0-9]\|[01]\{0,1\}[0-9][0-9]\{0,1\}\)' # ipRegex="\\($ipOctetRegex\\.$ipOctetRegex\\.$ipOctetRegex\\.$ipOctetRegex\\)" ipRegex='\([0-9]\{1,3\}\.\)\{3\}[0-9]\{1,3\}' else # Same as above. # ipRegex='\(\(0\.0\.0\.0\)\|\(127\.0\.0\.1\)\)' ipRegex='\(0\.0\.0\.0\)\{0,1\}\(127\.0\.0\.1\)\{0,1\}' fi domainRegex='\([0-9a-z_-]\{1,63\}\.\)\{1,\}[a-z][0-9a-z_-]\{1,62\}' blocklist=$(printf -- '%s' "$blocklist" | sed -n "/^\\(${ipRegex}[[:blank:]]\\{1,\\}\\)\\{0,1\\}$domainRegex$/p") logInfo 'Remove reserved TLDs' blocklist=$(printf -- '%s' "$blocklist" | sed \ -e '/\.example$/d' \ -e '/\.invalid$/d' \ -e '/\.local$/d' \ -e '/\.localdomain$/d' \ -e '/\.localhost$/d' \ -e '/\.test$/d' ) logInfo 'Remove destination IPs' blocklist=$(printf -- '%s' "$blocklist" | sed 's/^.\{1,\}[[:blank:]]\{1,\}//') if [ -n "$whitelist" ]; then logInfo 'Apply whitelist' for domain in $whitelist; do blocklist=$(printf -- '%s' "$blocklist" | sed "/$domain/d") done fi fi if [ -n "$blacklist" ]; then logInfo 'Apply blacklist' for domain in $blacklist; do blocklist=$(printf -- '%s\n%s' "$blocklist" "$domain") done fi # This domain is used to check if hBlock is disabled blocklist=$(printf -- '%s\n%s' "$blocklist" 'hblock-check.molinero.xyz') logInfo 'Remove empty lines' blocklist=$(printf -- '%s' "$blocklist" | sed '/^$/d') logInfo 'Sort entries' blocklist=$(printf -- '%s' "$blocklist" | sort | uniq) if [ -n "$redirection" ]; then logInfo 'Add new destination IP' # Escape string literal for use as the replacement string in sed escapedRedirection=$(printf -- '%s' "$redirection" | sed 's/[&/\]/\\&/g') blocklist=$(printf -- '%s' "$blocklist" | sed "s/^/$escapedRedirection /") fi if [ -f "$output" ]; then content=$(cat -- "$output") # Get custom section logAction 'Reading custom section...' custom=$( printf -- '%s' "$content" | sed '/^#.*/,/^#.*<\/custom>/!d;//d' && # "user-defined" is deprecated, use "custom" instead printf -- '%s' "$content" | sed '/^#.*/,/^#.*<\/user-defined>/!d;//d' ) # Backup procedure if [ "$backup" = true ]; then logAction 'Backing up hosts file...' [ -z "$backupDir" ] && backupDir=$(dirname -- "$output") writeFile "$content" "$backupDir/$(basename -- "$output").$(date +%s).bak" fi unset content fi generationDate=$(date -u) blocklistCount=$([ -n "$blocklist" ] && printf -- '%s\n' "$blocklist" | wc -l | tr -d '[:blank:]' || printf '0') logAction 'Generating hosts file...' # Hosts file hosts=$(cat <<-EOF # Author: Héctor Molinero Fernández # Repository: https://github.com/zant95/hblock # Last updated: $generationDate # Blocked domains: $blocklistCount $( [ -n "$header" ] && printf -- '\n%s\n%s\n%s\n' '#
' "$header" '#
' [ -n "$custom" ] && printf -- '\n%s\n%s\n%s\n' '# ' "$custom" '# ' [ -n "$blocklist" ] && printf -- '\n%s\n%s\n%s\n' '# ' "$blocklist" '# ' ) EOF ) writeFile "$hosts" "$output" logAction "$blocklistCount blocked domains!" } main "$@"