diff --git a/.gitignore b/.gitignore index eb684bc..31f4a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ response/error.txt +response/exit.txt +triggers/greet/greetings.txt +triggers/keywords/keywords.cfg log.txt .* bot.cfg diff --git a/bot.sh b/bot.sh index 0051538..c237b4c 100755 --- a/bot.sh +++ b/bot.sh @@ -18,6 +18,47 @@ if [[ ! -f "bot.cfg" ]]; then fi fi +# Check and initialize customizable configuration files +check_config_files() { + local configFiles=( + "response/error.txt:response/error.txt.example" + "response/exit.txt:response/exit.txt.example" + "triggers/greet/greetings.txt:triggers/greet/greetings.txt.example" + "triggers/keywords/keywords.cfg:triggers/keywords/keywords.cfg.example" + ) + + for entry in "${configFiles[@]}"; do + local target="${entry%:*}" + local example="${entry#*:}" + + if [[ ! -f "$target" ]]; then + if [[ -f "$example" ]]; then + cp "$example" "$target" + echo "Created $target from $example" + else + # Hardcoded fallback for critical files if example is missing + case "$target" in + "response/error.txt") + mkdir -p "$(dirname "$target")" + printf "I don't understand\nI'm not sure how to help you with that.\n" > "$target" + echo "Warning: $example not found. Created $target with default content." + ;; + "response/exit.txt") + mkdir -p "$(dirname "$target")" + printf "Ta ta for now.\nbye.\n" > "$target" + echo "Warning: $example not found. Created $target with default content." + ;; + *) + echo "Warning: Missing $example and no hardcoded fallback available." + ;; + esac + fi + fi + done +} + +check_config_files + # Load required files. for i in "bot.cfg" "functions.sh" ; do if [[ -f "$i" ]]; then @@ -231,6 +272,13 @@ while true; do from="${result#*#}" from="${from%% *}" from="#${from:-${channels[0]}}" + + # Validate channel name format (IRC RFC 2812: must start with #, contain only valid chars) + if [[ ! "$from" =~ ^#[a-zA-Z0-9_-]+$ ]]; then + # Invalid channel, use default + from="#${channels[0]}" + fi + # Trigger stuff happens here. # Call link trigger if msg contains a link: if [[ "$result" =~ .*http://|https://|www\..* ]]; then diff --git a/modules/convert/convert.sh b/modules/convert/convert.sh index 3221bae..a8bf36c 100755 --- a/modules/convert/convert.sh +++ b/modules/convert/convert.sh @@ -14,4 +14,11 @@ if ! check_dependencies "${dependencies[@]}"; then exit 1 fi -msg "$chan" "$(units -v ${*#* } | head -n1 | tr -d '[:space:]')" +# Validate input +if [[ -z "$*" ]]; then + msg "$chan" "Please provide a unit conversion (e.g., '10 meters to feet')." + exit 0 +fi + +# Quote variables to prevent command injection +msg "$chan" "$(units -v "${*#* }" | head -n1 | tr -d '[:space:]')" diff --git a/modules/fortune/fortune.sh b/modules/fortune/fortune.sh index 754137a..67c8afb 100755 --- a/modules/fortune/fortune.sh +++ b/modules/fortune/fortune.sh @@ -5,6 +5,9 @@ dependencies=("fortune") target="${3#fortune}" +# Trim leading/trailing whitespace +target="${target#"${target%%[![:space:]]*}"}" +target="${target%"${target##*[![:space:]]}"}" # Check dependencies before running if ! check_dependencies "${dependencies[@]}"; then @@ -12,6 +15,7 @@ if ! check_dependencies "${dependencies[@]}"; then exit 1 fi -fortuneText="$(fortune -a -e -s -n 512 $target || echo "No fortunes found.")" +# Quote target to prevent command injection +fortuneText="$(fortune -a -e -s -n 512 "$target" || echo "No fortunes found.")" fortuneText="$(echo "$fortuneText" | tr '[:space:]' ' ' | sed -e 's/"/\"/g')" msg "$2" "$fortuneText" diff --git a/modules/nick/nick.sh b/modules/nick/nick.sh index 59f9a3d..e3f3aa0 100755 --- a/modules/nick/nick.sh +++ b/modules/nick/nick.sh @@ -4,8 +4,35 @@ user=$1 shift shift -if [[ "$user" =~ $allowList ]]; then - ./modules/do/do.sh "$1" "#$channel" "does a magical gesture and turns into ${1}!" - nick $1 - sed -i bot.cfg -e "s/nick=.*/nick=\"$1\"/" + +newNick="$1" + +# Validate that user is authorized +if [[ ! "$user" =~ $allowList ]]; then + exit 0 fi + +# Validate IRC nickname format (RFC 2812) +# Nicknames can contain: a-z A-Z 0-9 _ - [ ] { } \ | ^ +if [[ -z "$newNick" ]]; then + msg "#$channel" "$user: Please provide a nickname." + exit 0 +fi + +if ! [[ "$newNick" =~ ^[a-zA-Z0-9_\[\]\{\}\\|\^-]+$ ]]; then + msg "#$channel" "$user: Invalid nickname format. Only alphanumeric and _-[]{}\\|^ allowed." + exit 0 +fi + +if [[ ${#newNick} -gt 30 ]]; then + msg "#$channel" "$user: Nickname too long (max 30 characters)." + exit 0 +fi + +# Change the nick +./modules/do/do.sh "$newNick" "#$channel" "does a magical gesture and turns into ${newNick}!" +nick "$newNick" + +# Safely update config file - escape forward slashes for sed +escapedNick="${newNick//\//\\/}" +sed -i "s/^nick=.*/nick=\"${escapedNick}\"/" bot.cfg diff --git a/modules/sing/sing.sh b/modules/sing/sing.sh index c5267cf..a4c6984 100755 --- a/modules/sing/sing.sh +++ b/modules/sing/sing.sh @@ -13,15 +13,15 @@ if ! check_dependencies "${dependencies[@]}"; then msg "$chan" "$1: This module requires: ${dependencies[*]}" exit 1 fi -#get the lyric text into a variable -lyricText="$(clyrics $@ | tr '[:space:]' ' ' | tr -s ' ' | fold -s -w 384)" +# Get the lyric text into a variable (quote $@ to prevent command injection) +lyricText="$(clyrics "$@" | tr '[:space:]' ' ' | tr -s ' ' | fold -s -w 384)" i=$(echo "$lyricText" | wc -l) -i=$(($RANDOM % $i + 1)) +i=$((RANDOM % i + 1)) lyricText="$(echo "$lyricText" | tail +$i | head -1 | rev | cut -d '.' -f2- | rev)" #Display the lyric text if [ ${#lyricText} -gt 15 ] ; then msg "$chan" "${lyricText}" exit 0 fi -msg "$chan" "no lyrics found for $@." +msg "$chan" "no lyrics found for $*." exit 0 diff --git a/response/error.txt b/response/error.txt.example similarity index 100% rename from response/error.txt rename to response/error.txt.example diff --git a/response/exit.txt b/response/exit.txt.example similarity index 100% rename from response/exit.txt rename to response/exit.txt.example diff --git a/triggers/greet/greet.sh b/triggers/greet/greet.sh index 2e511ce..e9b00b3 100755 --- a/triggers/greet/greet.sh +++ b/triggers/greet/greet.sh @@ -1,16 +1,26 @@ #!/usr/bin/env bash [ -f functions.sh ] && source functions.sh + +greetingsFile="triggers/greet/greetings.txt" + # All names to match are completely lowercase. case "${1,,}" in storm_dragon) msg "$2" "my lord, $1: how may I serve you?" ;; *) -greeting=( - Greetings - "Howdy, welcome to $2!" - "Wazzup Moe Fugger!" - "Welcome to $2!" -) -msg "$2" "$1: ${greeting[$(($RANDOM % ${#greeting[@]}))]}" +# Read greetings from file into array +if [[ -f "$greetingsFile" ]]; then + mapfile -t greeting < "$greetingsFile" +else + # Fallback if file doesn't exist + greeting=("Greetings" "Welcome!") +fi + +# Replace {channel} placeholder with actual channel name +selectedGreeting="${greeting[$((RANDOM % ${#greeting[@]}))]}" +selectedGreeting="${selectedGreeting//\{channel\}/$2}" + +msg "$2" "$1: $selectedGreeting" +;; esac diff --git a/triggers/greet/greetings.txt.example b/triggers/greet/greetings.txt.example new file mode 100644 index 0000000..dd96e81 --- /dev/null +++ b/triggers/greet/greetings.txt.example @@ -0,0 +1,4 @@ +Greetings +Howdy, welcome to {channel}! +Wazzup Moe Fugger! +Welcome to {channel}! diff --git a/triggers/keywords/keywords.cfg.example b/triggers/keywords/keywords.cfg.example new file mode 100644 index 0000000..1a578d0 --- /dev/null +++ b/triggers/keywords/keywords.cfg.example @@ -0,0 +1,26 @@ +# Keywords Configuration +# Format: keyword|action|percentage (percentage is optional, defaults to 100%) +# Available variables: $chan (channel), $who (user nickname) +# Use {random:option1|option2|option3} for random selection in messages +# +# Examples: +# word|msg "$chan" "Hello there!"|50% +# test|act "$chan" "does something cool" + +linux|msg "$chan" "Linux is {random:awesome|God|great|lovely|fantastic|amazing|wonderful}!"|25% +windows|msg "$chan" "{random:Failure is not an option, it comes bundled with Windows!|Apple got all pissed off because I farted in their store. It's not my falt they don't have Windows...|Windows is dumb!|Did you know that Micro Soft is Linda's pet name for Bill Gates?|A computer without Windows is like a chocolate cake without the mustard.|Windows is stupid|In a world without walls and fences - who needs windows and gates?|Windows, plug and pray.|Windows - Just another pain in the glass.|Windows, it's not pretty, it's not ugly, but it's pretty ugly.}!"|25% +emacs|msg "$chan" "$who, Real men of genius use vim!"|50% +jaws|msg "$chan" "${who}: watch out for sharks!" +emacspeak|msg "$chan" "$who, Real men of genius use vim!" +nano|msg "$chan" "$who, Real men of genius use vim!" +pidgin|msg "$chan" "$who, Real men of genius use irssi!" +weechat|msg "$chan" "$who, Real men of genius use irssi!" +thunderbird|msg "$chan" "$who, Real dogs use mutt, real men of genius use cat on a mailbox file!" +gedit|msg "$chan" "$who, Real men of genius use vim!" +pluma|msg "$chan" "$who, Real men of genius use vim!" +dragonforce|msg "$chan" "$who: I love DragonForce!!!" +vim|msg "$chan" "{random:Praise vim! HA|In times of trouble, just ask yourself, 'What would Bram Moolenaar do?'.|Vim is like a Ferrari, if you're a beginner, it handles like a bitch, but once you get the hang of it, it's small, powerful and FAST!|VIM is like a new model Ferrari, and sounds like one too - 'VIIIIIIMMM!'|Only through vim can you be saved! HA}" + +# Multi-word triggers (match anywhere in message, spaces removed) +# Format: ~phrase|action|percentage +~nowplaying:|act "$chan" "{random:cranks the volume up to 11|got soooo high at that show|boogies down to the sound of the band}!" diff --git a/triggers/keywords/keywords.sh b/triggers/keywords/keywords.sh index 1a91799..4d93fc5 100755 --- a/triggers/keywords/keywords.sh +++ b/triggers/keywords/keywords.sh @@ -4,67 +4,142 @@ who="${1%!*}" who="${who//:}" shift +# shellcheck disable=SC2034 # Used in action strings via execute_action chan="$1" shift -# each word is stored in an associative array, with the actions to be taken as the array's contents. -# the variable $chan contains the channel that caused the trigger. -# the variable $who contains the nick that caused the trigger. -# Optional: Add a percentage (e.g., "50%") as the last element to respond only that percent of the time. -declare -A keywords -keywords[linux]="msg \"$chan\" \"Linux is $(shuf -n1 -e awesome God great lovely fantastic amazing wonderful)!\" 25%" -keywords[windows]="msg \"$chan\" \"$(shuf -n1 -e\ - "Failure is not an option, it comes bundled with Windows!"\ - "Apple got all pissed off because I farted in their store. It's not my falt they don't have Windows..."\ - "Windows is dumb!"\ - "Did you know that Micro Soft is Linda's pet name for Bill Gates?"\ - "A computer without Windows is like a chocolate cake without the mustard."\ - "Windows is stupid"\ - "In a world without walls and fences - who needs windows and gates?"\ - "Windows, plug and pray."\ - "Windows - Just another pain in the glass."\ - "Windows, it's not pretty, it's not ugly, but it's pretty ugly.")!\" 25%" -keywords[emacs]="msg \"$chan\" \"$who, Real men of genius use vim!\" 50%" -keywords[jaws]="msg \"$chan\" \"${who}: watch out for sharks!\"" -keywords[emacspeak]="msg \"$chan\" \"$who, Real men of genius use vim!\"" -keywords[nano]="msg \"$chan\" \"$who, Real men of genius use vim!\"" -keywords[pidgin]="msg \"$chan\" \"$who, Real men of genius use irssi!\"" -keywords[weechat]="msg \"$chan\" \"$who, Real men of genius use irssi!\"" -keywords[thunderbird]="msg \"$chan\" \"$who, Real dogs use mutt, real men of genius use cat on a mailbox file!\"" -keywords[gedit]="msg \"$chan\" \"$who, Real men of genius use vim!\"" -keywords[pluma]="msg \"$chan\" \"$who, Real men of genius use vim!\"" -keywords[dragonforce]="msg \"$chan\" \"$who: I love DragonForce!!!\"" -keywords[vim]="msg \"$chan\" \"$(shuf -n1 -e \ - "Praise vim! HA"\ - "In times of trouble, just ask yourself, 'What would Bram Moolenaar do?'."\ - "Vim is like a Ferrari, if you're a beginner, it handles like a bitch, but once you get the hang of it, it's small, powerful and FAST!"\ - "VIM is like a new model Ferrari, and sounds like one too - 'VIIIIIIMMM!'"\ - "Only through vim can you be saved! HA")\"" +keywordsFile="triggers/keywords/keywords.cfg" +# Function to process random selection syntax: {random:opt1|opt2|opt3} +process_random() { + local text="$1" + while [[ "$text" =~ \{random:([^}]+)\} ]]; do + local options="${BASH_REMATCH[1]}" + IFS='|' read -ra optArray <<< "$options" + local selected="${optArray[$((RANDOM % ${#optArray[@]}))]}" + text="${text/\{random:$options\}/$selected}" + done + echo "$text" +} + +# Safe execution function - only allows predefined IRC functions +execute_action() { + local action="$1" + + # Parse the action to extract function name and arguments + if [[ "$action" =~ ^msg[[:space:]]+(\"[^\"]+\"|[^[:space:]]+)[[:space:]]+(.+)$ ]]; then + local target="${BASH_REMATCH[1]}" + local message="${BASH_REMATCH[2]}" + # Remove quotes from target and message if present + target="${target//\"/}" + # Safely expand only $chan and $who variables - NO EVAL + message="${message//\"\$chan\"/$chan}" + message="${message//\$chan/$chan}" + message="${message//\"\$who\"/$who}" + message="${message//\$who/$who}" + message="${message//\$\{chan\}/$chan}" + message="${message//\$\{who\}/$who}" + message="$(process_random "$message")" + msg "$target" "$message" + elif [[ "$action" =~ ^act[[:space:]]+(\"[^\"]+\"|[^[:space:]]+)[[:space:]]+(.+)$ ]]; then + local target="${BASH_REMATCH[1]}" + local message="${BASH_REMATCH[2]}" + # Remove quotes from target and message if present + target="${target//\"/}" + # Safely expand only $chan and $who variables - NO EVAL + message="${message//\"\$chan\"/$chan}" + message="${message//\$chan/$chan}" + message="${message//\"\$who\"/$who}" + message="${message//\$who/$who}" + message="${message//\$\{chan\}/$chan}" + message="${message//\$\{who\}/$who}" + message="$(process_random "$message")" + act "$target" "$message" + elif [[ "$action" =~ ^reply[[:space:]]+(\"[^\"]+\"|[^[:space:]]+)[[:space:]]+(.+)$ ]]; then + local target="${BASH_REMATCH[1]}" + local message="${BASH_REMATCH[2]}" + # Remove quotes from target and message if present + target="${target//\"/}" + # Safely expand only $chan and $who variables - NO EVAL + message="${message//\"\$chan\"/$chan}" + message="${message//\$chan/$chan}" + message="${message//\"\$who\"/$who}" + message="${message//\$who/$who}" + message="${message//\$\{chan\}/$chan}" + message="${message//\$\{who\}/$who}" + message="$(process_random "$message")" + reply "$target" "$message" + fi +} + +# Load keywords from config file into associative array +declare -A keywords +if [[ -f "$keywordsFile" ]]; then + while IFS='|' read -r keyword action percentage || [[ -n "$keyword" ]]; do + # Skip comments and empty lines + [[ "$keyword" =~ ^[[:space:]]*# ]] && continue + [[ -z "$keyword" ]] && continue + + # Trim whitespace + keyword="${keyword#"${keyword%%[![:space:]]*}"}" + keyword="${keyword%"${keyword##*[![:space:]]}"}" + action="${action#"${action%%[![:space:]]*}"}" + action="${action%"${action##*[![:space:]]}"}" + percentage="${percentage#"${percentage%%[![:space:]]*}"}" + percentage="${percentage%"${percentage##*[![:space:]]}"}" + + # Store in array (key includes ~ prefix for multi-word triggers) + if [[ -n "$percentage" ]]; then + keywords["$keyword"]="$action $percentage" + else + keywords["$keyword"]="$action" + fi + done < "$keywordsFile" +fi + +# Process single-word triggers wordList="$(echo "${@,,}" | tr '[:space:]' $'\n' | sort -u)" for w in ${wordList//[[:punct:]]/} ; do -if [[ -n "${keywords[${w,,}]}" && "$lastWordMatch" != "${keywords[${w,,}]}" ]]; then -keywordAction="${keywords[${w,,}]}" -# Check if the last element is a percentage -if [[ "$keywordAction" =~ (.*)\ ([0-9]+)%$ ]]; then -command="${BASH_REMATCH[1]}" -percentage="${BASH_REMATCH[2]}" -# Generate random number between 1-100 and only respond if within percentage -randomNum=$((RANDOM % 100 + 1)) -if [[ $randomNum -le $percentage ]]; then -eval "$command" -fi -else -# No percentage specified, always respond -eval "$keywordAction" -fi -lastWordMatch="${keywords[${w,,}]}" -fi + if [[ -n "${keywords[${w,,}]}" && "$lastWordMatch" != "${keywords[${w,,}]}" ]]; then + keywordAction="${keywords[${w,,}]}" + # Check if the last element is a percentage + if [[ "$keywordAction" =~ (.*)\ ([0-9]+)%$ ]]; then + command="${BASH_REMATCH[1]}" + percentage="${BASH_REMATCH[2]}" + # Generate random number between 1-100 and only respond if within percentage + randomNum=$((RANDOM % 100 + 1)) + if [[ $randomNum -le $percentage ]]; then + execute_action "$command" + fi + else + # No percentage specified, always respond + execute_action "$keywordAction" + fi + lastWordMatch="${keywords[${w,,}]}" + fi done -# Example of dealing with multi word triggers. -# Reset wordList without sorting it and with spaces removed. +# Process multi-word triggers (those starting with ~) wordList="$(echo "${@,,}" | tr -d '[:space:]')" -if [[ "${wordList,,}" =~ .*nowplaying:.* ]]; then -act "$chan" "$(shuf -n1 -e "cranks the volume up to 11" "got soooo high at that show" "boogies down to the sound of the band")!" -fi +for trigger in "${!keywords[@]}"; do + if [[ "$trigger" =~ ^~ ]]; then + # Remove the ~ prefix for matching + triggerPattern="${trigger#\~}" + if [[ "${wordList,,}" =~ .*${triggerPattern}.* ]]; then + keywordAction="${keywords[$trigger]}" + # Check if the last element is a percentage + if [[ "$keywordAction" =~ (.*)\ ([0-9]+)%$ ]]; then + command="${BASH_REMATCH[1]}" + percentage="${BASH_REMATCH[2]}" + # Generate random number between 1-100 and only respond if within percentage + randomNum=$((RANDOM % 100 + 1)) + if [[ $randomNum -le $percentage ]]; then + execute_action "$command" + fi + else + # No percentage specified, always respond + execute_action "$keywordAction" + fi + fi + fi +done diff --git a/triggers/link/link.sh b/triggers/link/link.sh index 8707b3a..270d3e2 100755 --- a/triggers/link/link.sh +++ b/triggers/link/link.sh @@ -12,10 +12,25 @@ fi for l in $3 ; do text="${l#:}" if [[ "${text}" =~ http://|https://|www\..* ]]; then -pageTitle="$(curl -L -s --connect-timeout 5 "$text" | sed -n -e 'H;${x;s!.*]*>\(.*\).*!\1!;T;s!.*\(.*\).*!\1!p}' | w3m -dump -T text/html | tr '[:space:]' ' ')" -pageTitle="$(echo "$pageTitle" | tr -cd '[:print:]')" -if [[ ${#pageTitle} -gt 1 ]]; then -msg "$2" "$pageTitle" -fi + # Security: Only allow http:// and https:// protocols + if [[ ! "$text" =~ ^https?:// ]]; then + # Convert www. to http://www. + if [[ "$text" =~ ^www\. ]]; then + text="http://$text" + else + # Skip unknown protocols + continue + fi + fi + + # Remove potentially dangerous shell metacharacters from URL + text="${text//[;&|]/}" + + # Fetch page title with timeout and security limits + pageTitle="$(curl -L -s --connect-timeout 5 --max-time 10 "$text" | sed -n -e 'H;${x;s!.*]*>\(.*\).*!\1!;T;s!.*\(.*\).*!\1!p}' | w3m -dump -T text/html | tr '[:space:]' ' ')" + pageTitle="$(echo "$pageTitle" | tr -cd '[:print:]')" + if [[ ${#pageTitle} -gt 1 ]]; then + msg "$2" "$pageTitle" + fi fi done