#!/usr/bin/env bash fail() { echo "$*" >&2 exit 1 } validate_single_line_value() { local label="$1" local value="$2" if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then fail "Invalid $label: control characters are not allowed." fi } validate_absolute_path() { local label="$1" local value="$2" validate_single_line_value "$label" "$value" [[ "$value" == /* ]] || fail "Invalid $label: expected an absolute path." [[ "$value" != *"//"* ]] || fail "Invalid $label: repeated slashes are not allowed." [[ "$value" != *"/./"* && "$value" != */. && "$value" != *"/../"* && "$value" != */.. ]] || fail "Invalid $label: dot path segments are not allowed." } validate_mount_source_path() { local label="$1" local value="$2" validate_absolute_path "$label" "$value" [[ "$value" != *:* ]] || fail "Invalid $label: ':' is not allowed in Podman bind-mount source paths." } ensure_safe_existing_dir() { local label="$1" local dir="$2" validate_absolute_path "$label" "$dir" [[ -d "$dir" ]] || fail "Missing $label: $dir" [[ ! -L "$dir" ]] || fail "Unsafe $label: symlinks are not allowed ($dir)" } stat_uid() { local path="$1" if stat -f '%u' "$path" >/dev/null 2>&1; then stat -f '%u' "$path" else stat -Lc '%u' "$path" fi } stat_mode() { local path="$1" if stat -f '%Lp' "$path" >/dev/null 2>&1; then stat -f '%Lp' "$path" else stat -Lc '%a' "$path" fi } ensure_private_existing_dir_owned_by_user() { local label="$1" local dir="$2" local uid="" local mode="" ensure_safe_existing_dir "$label" "$dir" uid="$(stat_uid "$dir")" [[ "$uid" == "$(id -u)" ]] || fail "Unsafe $label: not owned by current user ($dir)" mode="$(stat_mode "$dir")" (( (8#$mode & 0022) == 0 )) || fail "Unsafe $label: group/other writable ($dir)" } ensure_safe_write_file_path() { local label="$1" local file="$2" local dir validate_absolute_path "$label" "$file" if [[ -e "$file" ]]; then [[ ! -L "$file" ]] || fail "Unsafe $label: symlinks are not allowed ($file)" [[ -f "$file" ]] || fail "Unsafe $label: expected a regular file ($file)" fi dir="$(dirname "$file")" ensure_safe_existing_dir "${label} parent directory" "$dir" } write_file_atomically() { local file="$1" local mode="$2" local dir="" local tmp="" ensure_safe_write_file_path "output file" "$file" dir="$(dirname "$file")" tmp="$(mktemp "$dir/.tmp.XXXXXX")" cat >"$tmp" chmod "$mode" "$tmp" mv -f "$tmp" "$file" } validate_port() { local label="$1" local value="$2" local numeric="" [[ "$value" =~ ^[0-9]{1,5}$ ]] || fail "Invalid $label: must be numeric." numeric=$((10#$value)) (( numeric >= 1 && numeric <= 65535 )) || fail "Invalid $label: out of range." } resolve_user_home() { local user="$1" local home="" if command -v getent >/dev/null 2>&1; then home="$(getent passwd "$user" 2>/dev/null | cut -d: -f6 || true)" fi if [[ -z "$home" && -f /etc/passwd ]]; then home="$(awk -F: -v u="$user" '$1==u {print $6}' /etc/passwd 2>/dev/null || true)" fi if [[ -z "$home" ]]; then home="/home/$user" fi printf '%s' "$home" } generate_token_hex_32() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 return 0 fi if command -v python3 >/dev/null 2>&1; then python3 - <<'PY' import secrets print(secrets.token_hex(32)) PY return 0 fi if command -v od >/dev/null 2>&1; then od -An -N32 -tx1 /dev/urandom | tr -d " \n" return 0 fi echo "Missing dependency: need openssl or python3 (or od) to generate OPENCLAW_GATEWAY_TOKEN." >&2 exit 1 } upsert_env_var() { local file="$1" local key="$2" local value="$3" local tmp local dir ensure_safe_write_file_path "env file" "$file" dir="$(dirname "$file")" tmp="$(mktemp "$dir/.env.tmp.XXXXXX")" if [[ -f "$file" ]]; then awk -v k="$key" -v v="$value" ' BEGIN { found = 0 } $0 ~ ("^" k "=") { print k "=" v; found = 1; next } { print } END { if (!found) print k "=" v } ' "$file" >"$tmp" else printf '%s=%s\n' "$key" "$value" >"$tmp" fi mv "$tmp" "$file" chmod 600 "$file" 2>/dev/null || true }