Replace unsafe legacy Fileman symlink activation with an authenticated, root-owned account runner and crash-safe pointer reconciliation.
400 lines
12 KiB
Bash
Executable File
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
|