Files
pleno-vue/scripts/release/cpanel-activate.sh
T
Jeppe B 1a959c2ce8 Activate cPanel releases with a pinned account runner (#193)
Replace unsafe legacy Fileman symlink activation with an authenticated, root-owned account runner and crash-safe pointer reconciliation.
2026-07-20 18:20:03 +02:00

400 lines
12 KiB
Bash
Executable File

#!/bin/sh
set -u
umask 077
script_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
activation_root=${CPANEL_ACTIVATION_ROOT:-$script_directory}
activation_key_file=${CPANEL_ACTIVATION_KEY_FILE:-/etc/pleno-release-activator/truckwash.key}
case "$activation_root" in
""|"/") exit 1 ;;
esac
if [ ! -f "$activation_key_file" ]; then
exit 1
fi
IFS= read -r activation_key <"$activation_key_file" || [ -n "$activation_key" ] || exit 1
sha256_value_pending=$activation_key
if ! printf '%s\n' "$sha256_value_pending" | grep -Eq '^[a-f0-9]{64}$'; then
exit 1
fi
requests_directory="$activation_root/activation-requests"
results_directory="$activation_root/activation-results"
processed_directory="$requests_directory/processed"
state_directory="$activation_root/activation-state"
mkdir -p -- \
"$requests_directory" \
"$results_directory" \
"$processed_directory" \
"$state_directory" \
"$activation_root/archives" \
"$activation_root/releases" \
"$activation_root/staging" || exit 1
safe_component() {
printf '%s\n' "$1" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,179}$'
}
full_sha() {
printf '%s\n' "$1" | grep -Eq '^[a-f0-9]{40}$'
}
sha256_value() {
printf '%s\n' "$1" | grep -Eq '^[a-f0-9]{64}$'
}
decimal_timestamp() {
printf '%s\n' "$1" | grep -Eq '^[0-9]{10,12}$'
}
valid_target() {
candidate_target=$1
candidate_release=${candidate_target#releases/}
candidate_release=${candidate_release%/dist}
[ "$candidate_target" = "releases/$candidate_release/dist" ] &&
safe_component "$candidate_release"
}
target_identity() {
identity_target=$1
identity_release=${identity_target#releases/}
identity_release=${identity_release%/dist}
identity_commit=${identity_release%%-*}
identity_build=${identity_release#*-}
full_sha "$identity_commit" &&
[ "$identity_build" != "$identity_release" ] &&
safe_component "$identity_build"
}
validate_release() {
validated_target=$1
valid_target "$validated_target" || return 1
target_identity "$validated_target" || return 1
validated_dist="$activation_root/$validated_target"
[ -f "$validated_dist/index.html" ] &&
[ -f "$validated_dist/.htaccess" ] &&
[ -f "$validated_dist/release-manifest.json" ] &&
[ -f "$validated_dist/release-entry.json" ] &&
grep -Eq '^DirectoryIndex[[:space:]]+index\.html([[:space:]]|$)' "$validated_dist/.htaccess" &&
jq -e --arg commit "$identity_commit" --arg build "$identity_build" \
'.commit_sha == $commit and .build_id == $build' \
"$validated_dist/release-manifest.json" >/dev/null
}
write_result() {
result_status=$1
result_target=$2
result_message=$3
result_part="$results_directory/$request_id.result.part"
result_path="$results_directory/$request_id.result"
{
printf 'schema_version=1\n'
printf 'request_id=%s\n' "$request_id"
printf 'status=%s\n' "$result_status"
printf 'target=%s\n' "$result_target"
printf 'message=%s\n' "$result_message"
} >"$result_part" && mv -Tf -- "$result_part" "$result_path"
}
write_state() {
state_phase=$1
state_part="$state_directory/$request_id.state.part"
{
printf 'schema_version=1\n'
printf 'request_id=%s\n' "$request_id"
printf 'previous_target=%s\n' "$previous_target"
printf 'target=%s\n' "$target"
printf 'phase=%s\n' "$state_phase"
} >"$state_part" && mv -Tf -- "$state_part" "$state_path"
}
finish_request() {
final_status=$1
final_message=$2
write_result "$final_status" "$target" "$final_message" || exit 1
mv -Tf -- "$processing_path" "$processed_directory/$request_id.processed" || exit 1
[ "$final_status" = "success" ]
}
restore_previous() {
rollback_link="$activation_root/current.$request_id.rollback"
[ "$previous_target" != "$target" ] || return 1
validate_release "$previous_target" || return 1
if [ -L "$rollback_link" ]; then
[ "$(readlink "$rollback_link")" = "$previous_target" ] || return 1
elif [ -e "$rollback_link" ]; then
return 1
else
ln -s "$previous_target" "$rollback_link" || return 1
fi
mv -Tf -- "$rollback_link" "$activation_root/current" &&
[ "$(readlink "$activation_root/current")" = "$previous_target" ]
}
set -- "$requests_directory"/*.processing
if [ -e "$1" ]; then
processing_path=$1
request_name=${processing_path##*/}
request_id=${request_name%.processing}
else
set -- "$requests_directory"/*.request
[ -e "$1" ] || exit 0
request_path=$1
request_name=${request_path##*/}
request_id=${request_name%.request}
processing_path="$requests_directory/$request_id.processing"
if ! safe_component "$request_id" || [ -e "$processing_path" ]; then
exit 1
fi
mv -T -- "$request_path" "$processing_path" || exit 1
fi
if ! safe_component "$request_id"; then
exit 1
fi
schema_version=
parsed_request_id=
action=
release_id=
commit_sha=
build_id=
archive_name=
archive_sha256=
expires_at=
request_hmac=
parse_error=0
while IFS='=' read -r key value; do
case "$key" in
schema_version) [ -z "$schema_version" ] && schema_version=$value || parse_error=1 ;;
request_id) [ -z "$parsed_request_id" ] && parsed_request_id=$value || parse_error=1 ;;
action) [ -z "$action" ] && action=$value || parse_error=1 ;;
release_id) [ -z "$release_id" ] && release_id=$value || parse_error=1 ;;
commit_sha) [ -z "$commit_sha" ] && commit_sha=$value || parse_error=1 ;;
build_id) [ -z "$build_id" ] && build_id=$value || parse_error=1 ;;
archive_name) [ -z "$archive_name" ] && archive_name=$value || parse_error=1 ;;
archive_sha256) [ -z "$archive_sha256" ] && archive_sha256=$value || parse_error=1 ;;
expires_at) [ -z "$expires_at" ] && expires_at=$value || parse_error=1 ;;
request_hmac) [ -z "$request_hmac" ] && request_hmac=$value || parse_error=1 ;;
*) parse_error=1 ;;
esac
done <"$processing_path"
target="invalid"
activation_step=request_validation
activation_ok=0
expected_hmac=$(
{
printf 'schema_version=%s\n' "$schema_version"
printf 'request_id=%s\n' "$parsed_request_id"
printf 'action=%s\n' "$action"
printf 'release_id=%s\n' "$release_id"
printf 'commit_sha=%s\n' "$commit_sha"
printf 'build_id=%s\n' "$build_id"
printf 'archive_name=%s\n' "$archive_name"
printf 'archive_sha256=%s\n' "$archive_sha256"
printf 'expires_at=%s\n' "$expires_at"
} | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$activation_key" 2>/dev/null | awk '{print $NF}'
)
if [ "$parse_error" -eq 0 ] &&
[ "$schema_version" = "1" ] &&
[ "$parsed_request_id" = "$request_id" ] &&
safe_component "$release_id" &&
[ "$release_id" = "$commit_sha-$build_id" ] &&
full_sha "$commit_sha" &&
safe_component "$build_id" &&
decimal_timestamp "$expires_at" &&
sha256_value "$request_hmac" &&
[ "$request_hmac" = "$expected_hmac" ] &&
{ [ "$action" = "stage" ] || [ "$action" = "switch" ]; }; then
target="releases/$release_id/dist"
activation_ok=1
fi
state_path="$state_directory/$request_id.state"
previous_target=
state_phase=
if [ "$activation_ok" -eq 1 ] && [ -f "$state_path" ]; then
state_schema=
state_request_id=
state_previous_target=
state_target=
state_parse_error=0
while IFS='=' read -r key value; do
case "$key" in
schema_version) [ -z "$state_schema" ] && state_schema=$value || state_parse_error=1 ;;
request_id) [ -z "$state_request_id" ] && state_request_id=$value || state_parse_error=1 ;;
previous_target) [ -z "$state_previous_target" ] && state_previous_target=$value || state_parse_error=1 ;;
target) [ -z "$state_target" ] && state_target=$value || state_parse_error=1 ;;
phase) [ -z "$state_phase" ] && state_phase=$value || state_parse_error=1 ;;
*) state_parse_error=1 ;;
esac
done <"$state_path"
if [ "$state_parse_error" -ne 0 ] ||
[ "$state_schema" != "1" ] ||
[ "$state_request_id" != "$request_id" ] ||
[ "$state_target" != "$target" ] ||
! valid_target "$state_previous_target" ||
{ [ "$state_phase" != "prepared" ] && [ "$state_phase" != "activated" ]; }; then
activation_step=state_validation
activation_ok=0
else
previous_target=$state_previous_target
fi
fi
if [ "$activation_ok" -eq 1 ] && [ -z "$previous_target" ]; then
activation_step=current_validation
if [ ! -L "$activation_root/current" ]; then
activation_ok=0
else
previous_target=$(readlink "$activation_root/current")
if ! validate_release "$previous_target" || ! write_state prepared; then
activation_ok=0
else
state_phase=prepared
fi
fi
fi
if [ "$activation_ok" -eq 1 ] && [ "$(readlink "$activation_root/current" 2>/dev/null || true)" = "$target" ]; then
if validate_release "$target"; then
write_state activated || exit 1
finish_request success activated
exit $?
fi
if restore_previous; then
finish_request failure post_activation_validation
else
finish_request failure rollback_failed
fi
exit $?
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=request_expired
current_epoch=$(date +%s)
if [ "$expires_at" -lt "$current_epoch" ]; then
activation_ok=0
fi
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=atomic_preflight
probe="$activation_root/staging/.activation-preflight-$request_id"
if [ -e "$probe" ] || [ -L "$probe" ]; then
activation_ok=0
elif mkdir -p -- "$probe/first" "$probe/second" &&
: >"$probe/first/first-marker" &&
: >"$probe/second/second-marker" &&
ln -s first "$probe/current" &&
ln -s second "$probe/current.next" &&
mv -Tf -- "$probe/current.next" "$probe/current" &&
[ -f "$probe/current/second-marker" ] &&
[ ! -e "$probe/current/first-marker" ]; then
if ! rm -rf -- "$probe"; then
activation_ok=0
fi
else
activation_ok=0
fi
fi
release_root="$activation_root/releases/$release_id"
release_dist="$release_root/dist"
if [ "$activation_ok" -eq 1 ] && [ "$action" = "stage" ]; then
activation_step=archive_validation
if ! safe_component "$archive_name" ||
! sha256_value "$archive_sha256" ||
[ "${archive_name##*.}" != "zip" ]; then
activation_ok=0
fi
archive_path="$activation_root/archives/$archive_name"
staging_root="$activation_root/staging/$release_id.$request_id.pending"
if [ "$activation_ok" -eq 1 ]; then
activation_step=release_already_exists
if [ -e "$release_root" ] || [ -L "$release_root" ]; then
activation_ok=0
fi
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=archive_extraction
if [ -e "$staging_root" ] || [ -L "$staging_root" ] ||
[ ! -f "$archive_path" ] ||
[ "$(sha256sum "$archive_path" | awk '{print $1}')" != "$archive_sha256" ] ||
! mkdir -p -- "$staging_root" ||
! unzip -q "$archive_path" -d "$staging_root"; then
activation_ok=0
fi
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=release_validation
if [ -f "$staging_root/dist/index.html" ] &&
[ -f "$staging_root/dist/.htaccess" ] &&
[ -f "$staging_root/dist/release-manifest.json" ] &&
[ -f "$staging_root/dist/release-entry.json" ] &&
grep -Eq '^DirectoryIndex[[:space:]]+index\.html([[:space:]]|$)' "$staging_root/dist/.htaccess" &&
jq -e --arg commit "$commit_sha" --arg build "$build_id" \
'.commit_sha == $commit and .build_id == $build' \
"$staging_root/dist/release-manifest.json" >/dev/null &&
mv -T -- "$staging_root" "$release_root"; then
chmod 0755 "$release_dist"
else
activation_ok=0
fi
fi
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=release_validation
if ! validate_release "$target"; then
activation_ok=0
fi
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=request_expired
current_epoch=$(date +%s)
if [ "$expires_at" -lt "$current_epoch" ]; then
activation_ok=0
fi
fi
if [ "$activation_ok" -eq 1 ]; then
activation_step=atomic_activation
next_link="$activation_root/current.$request_id.next"
if [ -e "$next_link" ] || [ -L "$next_link" ]; then
activation_ok=0
elif ln -s "$target" "$next_link" &&
mv -Tf -- "$next_link" "$activation_root/current"; then
write_state activated || true
if [ "$(readlink "$activation_root/current")" = "$target" ] &&
validate_release "$target"; then
activation_ok=1
else
activation_ok=0
activation_step=post_activation_validation
if ! restore_previous; then
activation_step=rollback_failed
fi
fi
else
activation_ok=0
fi
fi
if [ "$activation_ok" -eq 1 ]; then
finish_request success activated
else
finish_request failure "$activation_step"
fi