diff --git a/.templates/00-global_var.sh b/.templates/00-global_var.sh index ddfc47de6..857e1feac 100755 --- a/.templates/00-global_var.sh +++ b/.templates/00-global_var.sh @@ -1,163 +1,290 @@ #!/usr/bin/with-contenv bashio # shellcheck shell=bash + set -e -if ! bashio::supervisor.ping 2> /dev/null; then +if ! bashio::supervisor.ping 2>/dev/null; then echo "..." exit 0 fi -################################### -# Export all addon options as env # -################################### - echo "" bashio::log.notice "This script converts all addon options to environment variables. Custom variables can be set using env_vars." bashio::log.notice "Additional informations : https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2" echo "" -# For all keys in options.json JSONSOURCE="/data/options.json" -# Define secrets location -if [ -f /homeassistant/secrets.yaml ]; then +# Define secrets location (optional) +SECRETSOURCE="" +if [[ -f /homeassistant/secrets.yaml ]]; then SECRETSOURCE="/homeassistant/secrets.yaml" -elif [ -f /config/secrets.yaml ]; then +elif [[ -f /config/secrets.yaml ]]; then SECRETSOURCE="/config/secrets.yaml" -else - SECRETSOURCE="false" fi -# Export keys as env variables -# echo "All addon options were exported as variables" -mapfile -t arr < <(jq -r 'keys[]' "${JSONSOURCE}") +# Injection block markers (single block, idempotent) +BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---" +BLOCK_END="# --- END ADDON ENV (generated) ---" -# Escape special characters using printf and enclose in double quotes -sanitize_variable() { - local raw="$1" - local escaped - if [[ "$raw" == \[* ]]; then - echo "One of your options is an array, skipping" - return +EXPORT_BLOCK_FILE="$(mktemp)" +trap 'rm -f "$EXPORT_BLOCK_FILE"' EXIT + +{ + echo "${BLOCK_BEGIN}" + echo "# Do not edit: generated from ${JSONSOURCE}" + echo "${BLOCK_END}" +} > "${EXPORT_BLOCK_FILE}" + +# Protected variables that should not be overwritten +declare -A PROTECTED_VARS=( + ["PATH"]=1 + ["HOME"]=1 + ["PWD"]=1 + ["SHLVL"]=1 + ["_"]=1 + ["S6_BEHAVIOR_IF_STAGE2_FAILS"]=1 +) + +is_valid_env_name() { + [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] +} + +# Quote for shell *code* (for injection into scripts). Keep punctuation intact. +# - single-line => single quotes +# - multi-line => $'...\n...' (one physical line; safe for injection) +shell_quote_for_code() { + local s="$1" + + if [[ "$s" == *$'\n'* || "$s" == *$'\r'* || "$s" == *$'\t'* ]]; then + s="${s//\\/\\\\}" + s="${s//\'/\\\'}" + s="${s//$'\n'/\\n}" + s="${s//$'\r'/\\r}" + s="${s//$'\t'/\\t}" + printf "\$'%s'" "$s" + return 0 fi - printf -v escaped '%q' "$raw" - # Do not espace spaces - escaped="${escaped//\\ / }" - if [[ "$raw" == "$escaped" ]]; then - printf '%s' "$raw" - else - printf '%s' "$escaped" + + # single-quote with embedded '"'"' for literal ' + s="${s//\'/\'\"\'\"\' }" + s="${s% }" + printf "'%s'" "$s" +} + +dotenv_quote() { + # For /.env and /etc/environment: double quotes + minimal escaping + local v="$1" + v="${v//\\/\\\\}" + v="${v//\"/\\\"}" + v="${v//$'\n'/\\n}" + v="${v//$'\r'/\\r}" + printf '"%s"' "$v" +} + +resolve_secret_if_needed() { + local v="$1" + local name line + + if [[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)[[:space:]]*$ ]]; then + name="${BASH_REMATCH[1]}" + name="${name#\"}"; name="${name%\"}" + name="${name#\'}"; name="${name%\'}" + + if [[ -z "${SECRETSOURCE}" ]]; then + bashio::log.warning "Homeassistant config not mounted, secrets are not supported" + printf '%s' "$v" + return 0 + fi + + # Exact key match at start of line; ignore comments + line="$( + awk -v k="$name" ' + /^[[:space:]]*#/ {next} + $0 ~ "^[[:space:]]*" k ":[[:space:]]*" { + sub("^[[:space:]]*" k ":[[:space:]]*", "", $0) + print + exit + } + ' "$SECRETSOURCE" + )" + + [[ -z "$line" ]] && bashio::exit.nok "Secret '${name}' not found in ${SECRETSOURCE}" + printf '%s' "$line" + return 0 fi + + printf '%s' "$v" +} + +append_export_line_for_injection() { + local key="$1" + local value="$2" + local quoted + quoted="$(shell_quote_for_code "$value")" + + awk -v k="$key" -v q="$quoted" -v end="$BLOCK_END" ' + $0 == end { print "export " k "=" q } + { print } + ' "${EXPORT_BLOCK_FILE}" > "${EXPORT_BLOCK_FILE}.tmp" + mv -f "${EXPORT_BLOCK_FILE}.tmp" "${EXPORT_BLOCK_FILE}" +} + +is_shell_run_script() { + local f="$1" + local h + h="$(head -n 1 "$f" 2>/dev/null || true)" + + [[ "$h" =~ ^#! ]] || return 1 + [[ "$h" =~ (sh|bash|with-contenv) ]] && return 0 + return 1 +} + +inject_block_into_file() { + local file="$1" + local tmp + tmp="$(mktemp)" + + awk -v bfile="${EXPORT_BLOCK_FILE}" -v begin="${BLOCK_BEGIN}" -v end="${BLOCK_END}" ' + function print_block() { + while ((getline l < bfile) > 0) print l + close(bfile) + } + BEGIN { inblock=0; printed=0 } + { + if ($0 == begin) { + inblock=1 + if (!printed) { print_block(); printed=1 } + next + } + if ($0 == end) { inblock=0; next } + if (inblock) next + + if (NR == 1) { + if ($0 ~ /^#!/) { + print $0 + if (!printed) { print_block(); printed=1 } + next + } else { + if (!printed) { print_block(); printed=1 } + print $0 + next + } + } + print $0 + } + END { + if (!printed) print_block() + } + ' "$file" > "$tmp" + + cat "$tmp" > "$file" + rm -f "$tmp" +} + +update_scripts_with_block() { + local f + local -A seen=() + + shopt -s nullglob + + # Added /etc/s6-overlay/s6-rc.d/*/run for newer S6 implementation (optional) + for f in /etc/services.d/*/run /etc/services.d/*/*run* /etc/cont-init.d/*.sh /etc/s6-overlay/s6-rc.d/*/run; do + [[ -f "$f" ]] || continue + [[ -n "${seen[$f]:-}" ]] && continue + seen["$f"]=1 + + if ! is_shell_run_script "$f"; then + bashio::log.debug "Skipping non-shell script: $f" + continue + fi + + inject_block_into_file "$f" + done + + shopt -u nullglob } export_option() { local key="$1" local value="$2" - local line secret secretnum valuepy - value=$(sanitize_variable "$value") - - if [[ -z "$value" ]]; then - line="${key}=''" - else - line="${key}='${value//\'/\'\\\'\'}'" + if [[ -n "${PROTECTED_VARS[$key]:-}" ]]; then + bashio::log.warning "Skipping protected environment variable: ${key}" + return 0 fi - if [[ "${line}" == *"!secret "* ]]; then - echo "secret detected" - secret=${line#*secret } - secret="${secret%[\"\']}" - if [[ "$SECRETSOURCE" == "false" ]]; then - bashio::log.warning "Homeassistant config not mounted, secrets are not supported" - return - fi - secretnum=$(sed -n "/$secret:/=" "$SECRETSOURCE") - [[ "$secretnum" == *' '* ]] && bashio::exit.nok "There are multiple matches for your password name. Please check your secrets.yaml file" - secret=$(sed -n "/$secret:/p" "$SECRETSOURCE") - secret=${secret#*: } - line="${line%%=*}='$secret'" - value="$secret" + if ! is_valid_env_name "$key"; then + bashio::log.warning "Skipping invalid env var name: ${key}" + return 0 fi - if bashio::config.false "verbose" || [[ "${key,,}" == *"pass"* ]]; then + value="$(resolve_secret_if_needed "$value")" + + if bashio::config.false "verbose" || [[ "${key,,}" =~ (pass|secret|token|apikey|api_key|private|pwd) ]]; then bashio::log.blue "${key}=******" else - bashio::log.blue "$line" + bashio::log.blue "${key}=${value}" fi - export "$line" + export "${key}=${value}" - if command -v "python3" &> /dev/null; then - [ ! -f /env.py ] && echo "import os" > /env.py - valuepy="${value//\\/\\\\}" - valuepy="${valuepy//[\"\']/}" - echo "os.environ['${key}'] = '$valuepy'" >> /env.py - python3 /env.py + if [[ -d /var/run/s6/container_environment ]]; then + printf '%s' "${value}" > "/var/run/s6/container_environment/${key}" fi - echo "$line" >> /.env || true + echo "${key}=$(dotenv_quote "$value")" >> "/.env" 2>/dev/null || true mkdir -p /etc - echo "$line" >> /etc/environment - if cat /etc/services.d/*/*run* &> /dev/null; then sed -i "1a export $line" /etc/services.d/*/*run* 2> /dev/null; fi - if cat /etc/cont-init.d/*.sh &> /dev/null; then sed -i "1a export $line" /etc/cont-init.d/*.sh 2> /dev/null; fi - if [ -d /var/run/s6/container_environment ]; then printf "%s" "${value}" > /var/run/s6/container_environment/"${key}"; fi - echo "export ${key}='${value}'" >> ~/.bashrc + echo "${key}=$(dotenv_quote "$value")" >> /etc/environment 2>/dev/null || true + + append_export_line_for_injection "$key" "$value" } +mapfile -t arr < <(jq -r 'keys[]' "${JSONSOURCE}") + for KEYS in "${arr[@]}"; do - # export key - VALUE=$(jq -r --raw-output ".\"$KEYS\"" "$JSONSOURCE") - # Check if the value is an array - if [[ "$VALUE" == \[* ]]; then + jtype="$(jq -r --arg k "$KEYS" '.[$k] | type' "$JSONSOURCE")" + + if [[ "$jtype" == "array" ]]; then if [[ "$KEYS" == "env_vars" ]]; then - mapfile -t env_entries < <(jq -c ".\"$KEYS\"[]" "$JSONSOURCE") - if [[ "${#env_entries[@]}" -eq 0 ]]; then - continue - fi - env_processed=false + mapfile -t env_entries < <(jq -c '.env_vars[]?' "$JSONSOURCE") for entry in "${env_entries[@]}"; do if [[ "$entry" == \{* ]]; then - env_name=$(jq -r 'if has("name") and has("value") then .name else empty end' <<< "$entry") + env_name="$(jq -r 'if has("name") and has("value") then .name else empty end' <<<"$entry")" if [[ -n "$env_name" ]]; then - env_value=$(jq -r '.value // empty' <<< "$entry") + env_value="$(jq -r '.value // "" | tostring' <<<"$entry")" export_option "$env_name" "$env_value" - env_processed=true - continue + else + mapfile -t env_keys < <(jq -r 'keys[]' <<<"$entry") + for env_key in "${env_keys[@]}"; do + env_value="$(jq -r --arg k "$env_key" '.[$k] // "" | tostring' <<<"$entry")" + export_option "$env_key" "$env_value" + done fi - - # Preserve multiline values: iterate keys and extract raw values without @tsv - mapfile -t env_keys < <(jq -r 'keys[]' <<< "$entry") - for env_key in "${env_keys[@]}"; do - # Use --arg to select the key; // empty to avoid "null" - env_value=$(jq -r --arg k "$env_key" '.[$k] // empty' <<< "$entry") - export_option "$env_key" "$env_value" - env_processed=true - done - elif [[ "${entry:0:1}" == '"' ]]; then - env_pair=$(jq -r '.' <<< "$entry") + else + env_pair="$(jq -r '.' <<<"$entry")" if [[ "$env_pair" == *=* ]]; then - env_key=${env_pair%%=*} - env_value=${env_pair#*=} - export_option "$env_key" "$env_value" - env_processed=true + export_option "${env_pair%%=*}" "${env_pair#*=}" else bashio::log.warning "env_vars entry '$env_pair' is not in KEY=VALUE format, skipping" fi - else - bashio::log.warning "env_vars entry format not supported, skipping" fi done - if [[ "$env_processed" == false ]]; then - bashio::log.warning "env_vars option format not supported, skipping" - fi else - bashio::log.warning "One of your option is an array, skipping" + bashio::log.warning "Option '${KEYS}' is an array, skipping" fi + elif [[ "$jtype" == "object" ]]; then + bashio::log.warning "Option '${KEYS}' is an object, skipping" + elif [[ "$jtype" == "null" ]]; then + continue else + VALUE="$(jq -r --arg k "$KEYS" '.[$k] // "" | tostring' "$JSONSOURCE")" export_option "$KEYS" "$VALUE" fi done +update_scripts_with_block + ################ # Set timezone # ################