#!/bin/sh ### NGINX Bad Bot Blocker: setup script ################# ### Copyright (C) 2017 Stuart Cardall ### ### https://github.com/itoffshore ### ### Licensed under the terms of the GPL2 ### ########################################################## WWW=/var/www VHOST_EXT="vhost" VHOST_DIR=/etc/nginx/sites-available BOTS_DIR=/etc/nginx/bots.d CONF_DIR=/etc/nginx/conf.d MAIN_CONF=/etc/nginx/nginx.conf # setting Y / yes will whitelist only directories in $www # that look like domain.names DOT_NAMES="Y" # if you already set 'limit_conn addr' you may want to set # this to N / no. INC_DDOS="Y" ####### end user configuration ########################### usage() { local script=$(basename $0) cat < exiting." exit 1 fi if [ ! -f "$MAIN_CONF" ]; then echo "NGINX main configuration ('$MAIN_CONF') not found => exiting." exit 1 fi } find_vhosts() { find $VHOST_DIR -type f -name "*.$VHOST_EXT" } whitelist_ips() { local ip= conf=$BOTS_DIR/whitelist-ips.conf mkdir -p $BOTS_DIR if [ -n "$(which dig)" ]; then ip=$(dig +short myip.opendns.com @resolver1.opendns.com) if ! grep "$ip" $conf &>/dev/null; then printf "%-17s %-15s %-s\n" "Whitelisting ip:" "$ip" "=> $conf" if [ "$DRY_RUN" = "N" ]; then printf "%-23s %-s\n" "$ip" "0;" >> $conf fi fi else echo "WARN: dig binary missing => install bind-tools to whitelist external ip address" fi } whitelist_domains() { local domain_list= domain= domain_len= local conf=$BOTS_DIR/whitelist-domains.conf case "$DOT_NAMES" in y*|Y*) domain_list=$(find $WWW -mindepth 1 -maxdepth 1 -type d -name '*\.*' -exec basename {} \;);; *) domain_list=$(find $WWW -mindepth 1 -maxdepth 1 -type d -exec basename {} \;);; esac domain_len=$(find $WWW -mindepth 1 -maxdepth 1 -type d -exec basename {} \; \ | awk '{ print length ($0) }' | sort -nr | head -1) for domain in $domain_list; do if ! grep "$domain" $conf &>/dev/null; then printf "%-s %-$(( $domain_len +2))s %s\n" "Whitelisting domain:" "$domain" "=> $conf" if [ "$DRY_RUN" = "N" ]; then printf "%-$(( $domain_len +8))s %s\n" "\"~*$domain\"" "0;" >> $conf fi fi done } longest_str() { echo $@ | tr " " "\n" | awk '{print length ($0)}' | sort -nr | head -n1 } add_includes() { local ph='<>' line=$1 file=$2 conf_dir=$3 text= update= local include_list=$(echo $@ | awk '{$1=$2=$3=""}sub("^"OFS"+","")') local col_size=$(( $(longest_str $include_list) + $(echo $conf_dir | wc -m) )) for text in $include_list; do if ! grep "$text" $file 1>/dev/null; then update='true' text="include $conf_dir/$text;" printf "%-10s %-$(( $col_size +10 ))s %s\n" "inserting:" "$text" "=> $file" if [ "$DRY_RUN" = "N" ]; then # $ph is just a placeholder so sed inserts a \t (tab) sed -i "$line i $ph \t$text $ph" $file fi fi done if [ "$DRY_RUN" = "N" ]; then if [ -n "$update" ]; then #add blank line below inserts line=$(( $line + $(echo $include_list | wc -w) )) if ! sed -n "${line}p" $file | grep ^'}' 1>/dev/null; then text="include $conf_dir/$(echo $include_list | awk '{print $1}');" sed -i "s|$text|$text\n|" $file fi #add comment above inserts text="include $conf_dir/$(echo $include_list | awk '{print $NF}');" sed -i "s|$text|# Bad Bot Blocker\n\t$text|" $file # remove placeholders sed -i "s|$ph||g" $file fi fi } find_line() { local file=$1 find_str=$2 first_last=$3 case "$first_last" in first) awk "/$find_str/{ print NR; exit }" $file;; last) awk "/$find_str/{ print NR }" $file | tail -n1;; esac } find_includes() { local file=$1 search=$2 search_first_last=$3 line= tmp=$(mktemp) local start_range=$4 start_first_last=$5 local end_range=$6 end_first_last=$7 local start=$(find_line $file $start_range $start_first_last) local end=$(find_line $file $end_range $end_first_last) sed -n ${start},${end}p ${file} > $tmp line=$(find_line $tmp $search $search_first_last) rm -f $tmp # search string not found if [ -z "$line" ]; then line=1 fi case "$search_first_last" in first) line=$(( $line + $start -1 ));; last) line=$(( $line + $start +1 ));; esac # if inserting beyond the end of the file if [ $line -gt $end ]; then # insert blank line sed -i "$end i \ " $file fi echo $line } sanitize_path() { echo $1 |tr -cd '[:alnum:] [=@=] [=.=] [=-=] [=/=] [=_=]' \ |tr -s '@.-/_' |awk '{print tolower($0)}' } sanitize_ext() { echo $1 |tr -cd '[:alnum:]' |awk '{print tolower($0)}' } check_args() { local option=$1 type=$2 arg=$3 local msg="ERROR: option '-$option' argument '$arg' requires:" case "$type" in path) if ! echo $arg | grep ^/ 1>/dev/null; then printf "$msg absolute path.\n" exit 1 fi ;; none) printf "$msg argument.\n"; exit 1;; esac } get_options() { local arg= opts= while getopts :w:e:v:b:c:m:ndxh opts "$@" do if [ -n "${OPTARG}" ]; then case "$opts" in e) arg=$(sanitize_ext ${OPTARG});; *) arg=$(sanitize_path ${OPTARG});; esac fi case "$opts" in w) WWW=$arg; check_args $opts path $arg ;; e) VHOST_EXT=$arg;; v) VHOST_DIR=$arg; check_args $opts path $arg ;; b) BOTS_DIR=$arg; check_args $opts path $arg ;; c) CONF_DIR=$arg; check_args $opts path $arg ;; m) MAIN_CONF=$arg; check_args $opts path $arg ;; n) DOT_NAMES=N ;; d) INC_DDOS=N ;; x) DRY_RUN=N ;; h) usage ;; \?) usage ;; :) check_args $OPTARG none none ;; esac done } wget_opts() { local opts= # GNU wget / Busybox 1.26.2 if wget --help 2>&1 | grep -q "\--spider"; then opts="--spider" else # Busybox wget < 1.26.2 opts="-s" fi echo $opts } check_online() { local url=$1 options=$(wget_opts) if wget $options $url &>/dev/null; then echo "true" fi } main() { local include_url= file= line= file_list= local CONF_FILES= BOT_FILES= local REPO=https://raw.githubusercontent.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker/master # require root if [ "$(id -u)" != "0" ]; then echo "This script must be run as root" 1>&2 exit 1 fi # parse command line get_options $@ include_url=$REPO/include_filelist.txt # check repo is online & source includes printf "Checking url: $include_url\n" if [ -n "$(check_online $include_url)" ]; then local tmp=$(mktemp) wget -q $include_url -O $tmp source $tmp 2>/dev/null rm -f $tmp else printf "Repo down or missing: $include_url\n" exit 1 fi # double check we have some files sourced if [ -z "$CONF_FILES" ] || [ -z "$BOT_FILES" ]; then printf "Error sourcing variables from: $include_url" exit 1 fi # configure ddos include case "$INC_DDOS" in n*|N*) BOT_FILES=$(echo $BOT_FILES | sed 's|ddos.conf||');; esac # gather vhosts file_list=$(find_vhosts) check_config $file_list # by default do not change any files if [ -z "$DRY_RUN" ]; then printf "\n** Dry Run ** | not updating files | -x or --exec to change files\n\n" fi # update vhosts for file in $file_list; do line=$(find_includes $file include last server_ last location first ) add_includes $line $file $BOTS_DIR $BOT_FILES done # update main config line=$(find_includes $MAIN_CONF include last http first '\}' last ) add_includes $line $MAIN_CONF $CONF_DIR $CONF_FILES whitelist_ips if [ -d $WWW ]; then whitelist_domains else echo "Web directory not found ('$WWW'): not whitelisting domains" fi } ## START ## main $@ exit $?