# Check if string is valid IPv4 is_ipv4() { local ip="$1" local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$" [[ "$ip" =~ $regex ]] } # Check if string is valid IPv4 with CIDR mask is_ipv4_cidr() { local ip="$1" local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(\/(3[0-2]|2[0-9]|1[0-9]|[0-9]))$" [[ "$ip" =~ $regex ]] } is_ipv4_ip_or_ipv4_cidr() { is_ipv4 "$1" || is_ipv4_cidr "$1" } is_domain() { local str="$1" local regex='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$' [[ "$str" =~ $regex ]] } is_domain_suffix() { local str="$1" local normalized="${str#.}" is_domain "$normalized" } # Checks if the given string is a valid base64-encoded sequence is_base64() { local str="$1" if echo "$str" | base64 -d > /dev/null 2>&1; then return 0 fi return 1 } # Checks if the given file exists file_exists() { local filepath="$1" if [[ -f "$filepath" ]]; then return 0 else return 1 fi } # Returns the inbound tag name by appending the postfix to the given section get_inbound_tag_by_section() { local section="$1" local postfix="in" echo "$section-$postfix" } # Returns the outbound tag name by appending the postfix to the given section get_outbound_tag_by_section() { local section="$1" local postfix="out" echo "$section-$postfix" } # Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix get_ruleset_tag() { local section="$1" local name="$2" local type="$3" local postfix="ruleset" if [ -n "$type" ]; then echo "$section-$name-$type-$postfix" else echo "$section-$name-$postfix" fi } # Determines the ruleset format based on the file extension (json → source, srs → binary) get_ruleset_format_by_file_extension() { local file_extension="$1" local format case "$file_extension" in json) format="source" ;; srs) format="binary" ;; *) log "Unsupported file extension: .$file_extension" return 1 ;; esac echo "$format" } # Converts a comma-separated string into a JSON array string comma_string_to_json_array() { local input="$1" if [ -z "$input" ]; then echo "[]" return fi local replaced="${input//,/\",\"}" echo "[\"$replaced\"]" } # Decodes a URL-encoded string url_decode() { local encoded="$1" printf '%b' "$(echo "$encoded" | sed 's/+/ /g; s/%/\\x/g')" } # Extracts the userinfo (username[:password]) part from a URL url_get_userinfo() { local url="$1" echo "$url" | sed -n -e 's#^[^:/?]*://##' -e '/@/!d' -e 's/@.*//p' } # Extracts the host part from a URL url_get_host() { local url="$1" echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#\([:/].*\|$\)##p' } # Extracts the port number from a URL url_get_port() { local url="$1" echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#^[^/]*:\([0-9][0-9]*\).*#\1#p' } # Extracts the path from a URL (without query or fragment; returns "/" if empty) url_get_path() { local url="$1" echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*##' -e 's#\([^?]*\).*#\1#p' } # Extracts the value of a specific query parameter from a URL url_get_query_param() { local url="$1" local param="$2" local raw raw=$(echo "$url" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p") [ -z "$raw" ] && echo "" && return echo "$raw" } # Extracts the basename (filename without extension) from a URL url_get_basename() { local url="$1" local filename="${url##*/}" local basename="${filename%%.*}" echo "$basename" } # Extracts and returns the file extension from the given URL url_get_file_extension() { local url="$1" local basename="${url##*/}" case "$basename" in *.*) echo "${basename##*.}" ;; *) echo "" ;; esac } # Decodes and returns a base64-encoded string base64_decode() { local str="$1" local decoded_url decoded_url="$(echo "$str" | base64 -d 2> /dev/null)" echo "$decoded_url" } # Generates a unique 16-character ID based on the current timestamp and a random number gen_id() { printf '%s%s' "$(date +%s)" "$RANDOM" | md5sum | cut -c1-16 } # Adds a missing UCI option with the given value if it does not exist migration_add_new_option() { local package="$1" local section="$2" local option="$3" local value="$4" local current current="$(uci -q get "$package.$section.$option")" if [ -z "$current" ]; then log "Adding missing option '$option' with value '$value'" uci set "$package.$section.$option=$value" uci commit "$package" return 0 else return 1 fi } # Migrates a configuration key in an OpenWrt config file from old_key_name to new_key_name migration_rename_config_key() { local config="$1" local key_type="$2" local old_key_name="$3" local new_key_name="$4" if grep -q "$key_type $old_key_name" "$config"; then log "Deprecated $key_type found: $old_key_name migrating to $new_key_name" sed -i "s/$key_type $old_key_name/$key_type $new_key_name/g" "$config" fi } # Download URL content directly download_to_stream() { local url="$1" local http_proxy_address="$2" local retries="${3:-3}" local wait="${4:-2}" for attempt in $(seq 1 "$retries"); do if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -qO- "$url" | sed 's/\r$//' && break else wget -qO- "$url" | sed 's/\r$//' && break fi log "Attempt $attempt/$retries to download $url failed" "warn" sleep "$wait" done } # Download URL to file download_to_file() { local url="$1" local filepath="$2" local http_proxy_address="$3" local retries="${4:-3}" local wait="${5:-2}" for attempt in $(seq 1 "$retries"); do if [ -n "$http_proxy_address" ]; then http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -O "$filepath" "$url" && break else wget -O "$filepath" "$url" && break fi log "Attempt $attempt/$retries to download $url failed" "warn" sleep "$wait" done if grep -q $'\r' "$filepath"; then log "Downloaded file has Windows line endings (CRLF). Converting to Unix (LF)" sed -i 's/\r$//' "$filepath" fi } # Decompiles a sing-box SRS binary file into a JSON ruleset file decompile_srs_file() { local binary_filepath="$1" local output_filepath="$2" log "Decompiling $binary_filepath to $output_filepath" "debug" if ! file_exists "$binary_filepath"; then log "File $binary_filepath not found" "error" return 1 fi sing-box rule-set decompile "$binary_filepath" -o "$output_filepath" if [[ $? -ne 0 ]]; then log "Decompilation command failed for $binary_filepath" "error" return 1 fi } ####################################### # Parses a whitespace-separated string, validates items as either domains # or IPv4 addresses/subnets, and returns a comma-separated string of valid items. # Arguments: # $1 - Input string (space-separated list of items) # $2 - Type of validation ("domains" or "subnets") # Outputs: # Comma-separated string of valid domains or subnets ####################################### parse_domain_or_subnet_string_to_commas_string() { local string="$1" local type="$2" tmpfile=$(mktemp) printf "%s\n" "$string" | sed 's/\/\/.*//' | tr ', ' '\n' | grep -v '^$' > "$tmpfile" result="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "$type")" rm -f "$tmpfile" echo "$result" } ####################################### # Parses a file line by line, validates entries as either domains or subnets, # and returns a single comma-separated string of valid items. # Arguments: # $1 - Path to the input file # $2 - Type of validation ("domains" or "subnets") # Outputs: # Comma-separated string of valid domains or subnets ####################################### parse_domain_or_subnet_file_to_comma_string() { local filepath="$1" local type="$2" local result while IFS= read -r line; do line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') [ -z "$line" ] && continue case "$type" in domains) if ! is_domain_suffix "$line"; then log "'$line' is not a valid domain" "debug" continue fi ;; subnets) if ! is_ipv4 "$line" && ! is_ipv4_cidr "$line"; then log "'$line' is not IPv4 or IPv4 CIDR" "debug" continue fi ;; *) log "Unknown type: $type" "error" return 1 ;; esac if [ -z "$result" ]; then result="$line" else result="$result,$line" fi done < "$filepath" echo "$result" }