openclaw/scripts/podman/common.sh
Peter Steinberger 2291fe823e
refactor(scripts): deslop tooling scripts second pass (#161169)
Share repeated tooling parsing, projections, and fixture transforms while preserving command and generated-output contracts. Repair the OpenGrep help range so bootstrap code no longer replaces documented usage.
2026-09-29 12:05:18 +00:00

215 lines
6.1 KiB
Bash

#!/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"
}
write_local_control_ui_origins() {
python3 - "$@" <<'PY'
import json
import sys
path, port, tmp, operation = sys.argv[1:]
try:
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
except json.JSONDecodeError as exc:
print(
f"Warning: unable to {operation} gateway.controlUi.allowedOrigins in {path}: existing config is not strict JSON ({exc}). Leaving file unchanged.",
file=sys.stderr,
)
raise SystemExit(1)
if not isinstance(data, dict):
raise SystemExit(f"{path}: expected top-level object")
gateway = data.setdefault("gateway", {})
if not isinstance(gateway, dict):
raise SystemExit(f"{path}: expected gateway object")
gateway.setdefault("mode", "local")
control_ui = gateway.setdefault("controlUi", {})
if not isinstance(control_ui, dict):
raise SystemExit(f"{path}: expected gateway.controlUi object")
allowed = control_ui.get("allowedOrigins")
public_origin = gateway.get("publicOrigin")
inherits_public_origin = "allowedOrigins" not in control_ui and isinstance(public_origin, str) and public_origin.strip()
desired = [f"http://127.0.0.1:{port}", f"http://localhost:{port}"]
if not isinstance(allowed, list):
allowed = []
cleaned = []
seen = set()
# Setup replaces managed localhost ports; launch preserves and deduplicates them.
for origin in allowed + (desired if operation == "sync" else []):
if not isinstance(origin, str):
continue
normalized = origin.strip()
if not normalized or (operation == "sync" and normalized in seen):
continue
if operation == "seed" and normalized.startswith("http://"):
host = normalized[len("http://") :].split(":", 1)[0]
if host in {"127.0.0.1", "localhost"}:
continue
cleaned.append(normalized)
seen.add(normalized)
if not inherits_public_origin:
control_ui["allowedOrigins"] = cleaned + (desired if operation == "seed" else [])
with open(tmp, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2)
fh.write("\n")
PY
}
generate_token_hex_32() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
return $?
fi
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PY'
import secrets
print(secrets.token_hex(32))
PY
return $?
fi
if command -v od >/dev/null 2>&1; then
od -An -N32 -tx1 /dev/urandom | tr -d " \n"
return $?
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
}