# Remove a single trailing slash from a string. # $1 (s): input string. function strTrimSlash() { local s="${1-}" s="${s%/}" printf '%s' "$s" } # Generates a random password with a specified length and character set. # # $1 (length): length of the password (defaults to 32 characters). # $2 (charset): string of characters to use for generating the password (defaults to "A-Za-z0-9@#$%^*_+=[]{}:"). function strRandom() { local length="${1:-32}" local charset="${2:-"A-Za-z0-9_.-"}" LC_ALL=C tr -dc "$charset" < /dev/urandom | head -c "$length" } # Format a number with thousands separators (space-separated groups). # $1 (number): the number to format. function numFormat() { printf '%s\n' "${1-}" | sed ':a;s/\B[0-9]\{3\}\>/ &/;ta' } # Check if a value exists in an array passed as remaining arguments. # $1 (needle): value to search for. # $2 (items): array elements to search in (passed as individual arguments). function arrayContains() { local needle="${1-}" shift local item for item in "$@"; do [[ "$item" == "$needle" ]] && return 0 done return 1 } # Return success if $value exists as full line in multiline $list function listContains() { local value="${1-}" local list="${2-}" [[ -n "$value" ]] || return 1 grep -Fxq -- "$value" <<<"$list" } # Calculate the difference in seconds between two datetime strings. # $1 (from): start datetime string (accepted by date -d). # $2 (to): end datetime string (accepted by date -d). function timeDiff() { local from="${1-}" local to="${2-}" local t1 t2 [[ -n "$from" ]] || { appError "time_from not specified"; return 1; } [[ -n "$to" ]] || { appError "time_to not specified"; return 1; } t1="$(date -d "$from" +%s)" || { appError "invalid time_from: $from"; return 1; } t2="$(date -d "$to" +%s)" || { appError "invalid time_to: $to"; return 1; } printf '%s\n' "$(( t2 - t1 ))" } # Normalize a domain name: lowercase, strip whitespace and trailing dot, then IDN-encode. # $1 (domain): raw domain string to normalize. function domainPrepare() { local domain="${1-}" domain="${domain,,}" domain="${domain//[[:space:]]/}" domain="${domain%.}" LC_ALL=C.UTF-8 idn2 <<<"$domain" } # Checks if the given string is a valid domain. # # $1 (domain): a string representing the domain. function domainValidate() { local domain="${1-}" (( ${#domain} >= 4 )) || { appError "Domain name is too short: $domain"; return 1; } (( ${#domain} <= 253 )) || { appError "Domain name is too long: $domain"; return 1; } [[ "$domain" =~ ^([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?\.)+([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})$ ]] || { appError "Invalid domain name format: $domain"; return 1; } } # Validate a domain name and reject reserved names. # $1 (domain): domain name to check. function domainCheck() { local domain="${1-}" local reserved=("root" "user") arrayContains "$domain" "${reserved[@]}" && { appError "Reserved domain name: $domain"; return 1; } domainValidate "$domain" } # Generate username and groupname from domain function domainToUser() { local domain="${1-}" local base hash base="$(printf '%s' "$domain" \ | tr '[:upper:]' '[:lower:]' \ | sed -E 's/[^a-z0-9-]+/-/g; s/-{2,}/-/g; s/^-//; s/-$//')" hash="$(printf '%s' "$domain$hostSalt" \ | sha256sum \ | awk '{print substr($1,1,8)}')" printf '%s' "${base:0:21}-${hash}" } function domainToUid() { local domain="${1-}" local username uid if [[ -z "$domain" ]]; then appError "Domain not specified" return 1 fi username=$(domainToUser "$domain") if [[ -z "$username" ]]; then appError "Cannot compute username for: $domain" return 1 fi uid=$(id -u "$username" 2>/dev/null) if [[ -z "$uid" ]]; then appError "No such user: $username" return 1 fi if [[ "$uid" == "0" ]]; then appError "Refusing to return root UID for: $domain" return 1 fi printf '%s' "$uid" } function domainToGid() { local domain="${1-}" local username gid if [[ -z "$domain" ]]; then appError "Domain not specified" return 1 fi username=$(domainToUser "$domain") if [[ -z "$username" ]]; then appError "Cannot compute username for: $domain" return 1 fi gid=$(id -g "$username" 2>/dev/null) if [[ -z "$gid" ]]; then appError "No such user: $username" return 1 fi if [[ "$gid" == "0" ]]; then appError "Refusing to return root GID for: $domain" return 1 fi printf '%s' "$gid" } # Generate random string from domain function domainToRandom() { local domain="${1-}" local maxLen="${2:-256}" local tailLen="${3:-8}" local base tail baseLen base="$(printf '%s' "$domain" \ | tr '[:upper:]' '[:lower:]' \ | sed -E 's/[^a-z0-9]+/_/g; s/_{2,}/_/g; s/^_//; s/_$//')" tail="$(strRandom "$tailLen" 'a-z0-9')" || return 1 baseLen=$(( maxLen - tailLen - 1 )) (( baseLen < 1 )) && baseLen=1 printf '%s' "${base:0:baseLen}_${tail}" } # Run a command and capture its stdout and stderr into named variables. # $1 (outVar): name of the variable to receive stdout. # $2 (errVar): name of the variable to receive stderr. # $3 (cmd): command and arguments to execute. function run() { local -n __runOut="$1" local -n __runError="$2" shift 2 local stdoutTmp stderrTmp status stdoutTmp=$(mktemp) || return 1 stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; } "$@" >"$stdoutTmp" 2>"$stderrTmp" status=$? __runOut=$(<"$stdoutTmp") __runError=$(<"$stderrTmp") rm -f "$stdoutTmp" "$stderrTmp" return "$status" } # Run command, discard stdout function runError() { local -n __runErrorErr="$1" shift local __runErrorOutput __runErrorError status run __runErrorOutput __runErrorError "$@" status=$? if [[ -n "$__runErrorError" ]]; then __runErrorErr="$__runErrorError" else __runErrorErr="$__runErrorOutput" fi return "$status" } # Run a command, discarding output, and print an error message on failure. # $1 (cmd): command and arguments to execute. function runFail() { local __runFailErr runError __runFailErr "$@" local status=$? [[ $status -eq 0 ]] || appError "$__runFailErr" return $status } # Run a command and discard all stdout and stderr output. # $1 (cmd): command and arguments to execute. function runSilent() { local output error run output error "$@" return $? } # Run a shell command string (with operator support) and capture stdout and stderr into named variables. # $1 (outVar): name of the variable to receive stdout. # $2 (errVar): name of the variable to receive stderr. # $3 (cmd): command and arguments, including shell operators (|, &&, ;, etc.). function runShell() { local -n __out="$1" local -n __err="$2" shift 2 local stdoutTmp stderrTmp status stdoutTmp=$(mktemp) || return 1 stderrTmp=$(mktemp) || { rm -f "$stdoutTmp"; return 1; } # Arguments: > >> < | || & && ; ( ) convert to operators. local cmd="" arg for arg in "$@"; do if [[ "$arg" =~ ^([0-9]?>|[0-9]?>>|[0-9]?<|\||\|\||&&|;|\(|\))$ ]]; then cmd+=" $arg" else cmd+=" $(printf '%q' "$arg")" fi done ( set -o pipefail eval -- "$cmd" ) >"$stdoutTmp" 2>"$stderrTmp" status=$? __out=$(<"$stdoutTmp") __err=$(<"$stderrTmp") rm -f "$stdoutTmp" "$stderrTmp" return "$status" } # Converts notation (28Gi, 512Mi, 1Ki, 4Gb, ...) to bytes; returns -1 for empty input. function sizeToBytes() { local v="$1" case "$v" in *Ti) echo $(( ${v%Ti} * 1099511627776 )) ;; *Gi) echo $(( ${v%Gi} * 1073741824 )) ;; *Mi) echo $(( ${v%Mi} * 1048576 )) ;; *Ki) echo $(( ${v%Ki} * 1024 )) ;; *Tb) echo $(( ${v%Tb} * 1000000000000 )) ;; *Gb) echo $(( ${v%Gb} * 1000000000 )) ;; *Mb) echo $(( ${v%Mb} * 1000000 )) ;; *Kb) echo $(( ${v%Kb} * 1000 )) ;; "") echo -1 ;; *) echo "$v" ;; esac } #========================================================================= # Comment out the first non-empty, non-commented line in the domains list file. # Prints the domain name on success, returns 1 if no domain available. function domainsListMark() { local file="$1" local line domain num=0 lineNum=0 [[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; } while IFS= read -r line; do (( num++ )) [[ -z "$line" || "$line" == \#* ]] && continue domain="$line" lineNum=$num break done < "$file" [[ $lineNum -eq 0 ]] && return 1 sed -i "${lineNum}s/^/#/" "$file" printf '%s\n' "$domain" } # Remove the '#' prefix from the specified domain line in the domains list file. function domainsListUnmark() { local file="$1" local domain="$2" local line num=0 lineNum=0 [[ -f "$file" ]] || { appError "Domains list file not found: $file"; return 1; } [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } while IFS= read -r line; do (( num++ )) [[ "$line" == "#${domain}" ]] && { lineNum=$num; break; } done < "$file" [[ $lineNum -eq 0 ]] && { appError "Domain not marked in list: $domain"; return 1; } sed -i "${lineNum}s/^#//" "$file" } # Sends an email with the specified subject and body. # # $1 (mailSubject): string containing the subject of the email. # $2 (mailBody): string containing the body of the email. function emailSend() { local mailSubject="$1" local mailBody="$2" [[ -n "$mailSubject" ]] || { appError "Subject not specified"; return 1; } [[ -n "$mailBody" ]] || { appError "Body email not specified"; return 1; } local result result=$(printf 'Subject:%s\nFrom:%s\nTo:%s\n\n%s' "$mailSubject" "$mailFrom" "$mailTo" "$mailBody" | msmtp "$mailTo" 2>&1) [[ $? -eq 0 ]] || { appError "$result"; return 1; } }