mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
494 lines
15 KiB
Bash
Executable File
494 lines
15 KiB
Bash
Executable File
#!/bin/sh
|
|
# shellcheck shell=ash
|
|
|
|
# Variables are populated by sourced SafeShield helpers and ss_load_config().
|
|
# shellcheck disable=SC2154
|
|
LC_ALL=C
|
|
export LC_ALL
|
|
umask 077
|
|
|
|
SS_STATS_UPLOAD_FUNCTIONS_LIB="${SS_STATS_UPLOAD_FUNCTIONS_LIB:-/lib/functions.sh}"
|
|
SS_STATS_UPLOAD_CORE_LIB="${SS_STATS_UPLOAD_CORE_LIB:-/usr/lib/safeshield/core.sh}"
|
|
SS_STATS_UPLOAD_INTERVAL_S="${SS_STATS_UPLOAD_INTERVAL_S:-1800}"
|
|
SS_STATS_UPLOAD_IDLE_WAIT_S="${SS_STATS_UPLOAD_IDLE_WAIT_S:-15}"
|
|
SS_STATS_UPLOAD_RETRY_DELAY_S="${SS_STATS_UPLOAD_RETRY_DELAY_S:-2}"
|
|
SS_STATS_UPLOAD_FULL_EVERY="${SS_STATS_UPLOAD_FULL_EVERY:-23}"
|
|
SS_STATS_UPLOAD_DENIED_RECHECK_S="${SS_STATS_UPLOAD_DENIED_RECHECK_S:-43200}"
|
|
SS_STATS_UPLOAD_RESPONSE_FILE="${SS_STATS_UPLOAD_RESPONSE_FILE:-/tmp/safeshield/statistics/upload.response.json}"
|
|
|
|
# shellcheck disable=SC1090
|
|
. "$SS_STATS_UPLOAD_FUNCTIONS_LIB"
|
|
# shellcheck disable=SC1090
|
|
. "$SS_STATS_UPLOAD_CORE_LIB"
|
|
|
|
ss_should_terminate=0
|
|
upload_sleep_pid=''
|
|
upload_state_generation=''
|
|
upload_state_last_success_seq=0
|
|
upload_state_last_full_seq=0
|
|
upload_state_recent_successes=0
|
|
upload_state_needs_full=1
|
|
upload_pending_mode=''
|
|
upload_pending_generation=''
|
|
upload_pending_seq=0
|
|
|
|
upload_handle_term() {
|
|
ss_should_terminate=1
|
|
if [ -n "$upload_sleep_pid" ]; then
|
|
kill "$upload_sleep_pid" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
upload_sleep() {
|
|
local seconds="$1"
|
|
local rc=0
|
|
|
|
[ "$ss_should_terminate" -eq 1 ] && return 1
|
|
[ "$seconds" -gt 0 ] 2>/dev/null || return 0
|
|
|
|
sleep "$seconds" &
|
|
upload_sleep_pid=$!
|
|
wait "$upload_sleep_pid" 2>/dev/null || rc=$?
|
|
upload_sleep_pid=''
|
|
|
|
[ "$ss_should_terminate" -eq 1 ] && return 1
|
|
[ "$rc" -eq 0 ]
|
|
}
|
|
|
|
upload_state_reset() {
|
|
upload_state_generation=''
|
|
upload_state_last_success_seq=0
|
|
upload_state_last_full_seq=0
|
|
upload_state_recent_successes=0
|
|
upload_state_needs_full=1
|
|
}
|
|
|
|
upload_state_load() {
|
|
local old_ifs
|
|
|
|
upload_state_reset
|
|
[ -s "$SS_STATISTICS_UPLOAD_STATE_FILE" ] || return 0
|
|
|
|
old_ifs="$IFS"
|
|
IFS="$(printf '\t')"
|
|
read -r upload_state_generation upload_state_last_success_seq upload_state_last_full_seq upload_state_recent_successes upload_state_needs_full <"$SS_STATISTICS_UPLOAD_STATE_FILE" || true
|
|
IFS="$old_ifs"
|
|
|
|
is_valid_integer "$upload_state_last_success_seq" || upload_state_last_success_seq=0
|
|
is_valid_integer "$upload_state_last_full_seq" || upload_state_last_full_seq=0
|
|
is_valid_integer "$upload_state_recent_successes" || upload_state_recent_successes=0
|
|
case "$upload_state_needs_full" in
|
|
0 | 1) ;;
|
|
*) upload_state_needs_full=1 ;;
|
|
esac
|
|
}
|
|
|
|
upload_state_save() {
|
|
local tmp="${SS_STATISTICS_UPLOAD_STATE_FILE}.tmp.$$"
|
|
|
|
mkdir -p "${SS_STATISTICS_UPLOAD_STATE_FILE%/*}" || return 1
|
|
printf '%s\t%s\t%s\t%s\t%s\n' \
|
|
"$upload_state_generation" \
|
|
"$upload_state_last_success_seq" \
|
|
"$upload_state_last_full_seq" \
|
|
"$upload_state_recent_successes" \
|
|
"$upload_state_needs_full" >"$tmp" || {
|
|
rm -f "$tmp"
|
|
return 1
|
|
}
|
|
mv -f "$tmp" "$SS_STATISTICS_UPLOAD_STATE_FILE" || {
|
|
rm -f "$tmp"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
upload_mark_needs_full() {
|
|
local generation="${1:-$upload_state_generation}"
|
|
|
|
if [ -n "$generation" ] && [ "$upload_state_generation" != "$generation" ]; then
|
|
upload_state_generation="$generation"
|
|
upload_state_last_success_seq=0
|
|
upload_state_last_full_seq=0
|
|
upload_state_recent_successes=0
|
|
fi
|
|
upload_state_needs_full=1
|
|
upload_state_save >/dev/null 2>&1 || true
|
|
}
|
|
|
|
upload_pending_clear() {
|
|
rm -f "$SS_STATISTICS_UPLOAD_PENDING_FILE" "$SS_STATISTICS_UPLOAD_PENDING_META_FILE"
|
|
upload_pending_mode=''
|
|
upload_pending_generation=''
|
|
upload_pending_seq=0
|
|
}
|
|
|
|
upload_pending_load() {
|
|
local old_ifs payload_generation payload_seq
|
|
|
|
upload_pending_mode=''
|
|
upload_pending_generation=''
|
|
upload_pending_seq=0
|
|
[ -s "$SS_STATISTICS_UPLOAD_PENDING_FILE" ] || return 1
|
|
[ -s "$SS_STATISTICS_UPLOAD_PENDING_META_FILE" ] || {
|
|
upload_pending_clear
|
|
return 1
|
|
}
|
|
|
|
old_ifs="$IFS"
|
|
IFS="$(printf '\t')"
|
|
read -r upload_pending_mode upload_pending_generation upload_pending_seq <"$SS_STATISTICS_UPLOAD_PENDING_META_FILE" || true
|
|
IFS="$old_ifs"
|
|
|
|
case "$upload_pending_mode" in
|
|
full | recent) ;;
|
|
*)
|
|
upload_pending_clear
|
|
return 1
|
|
;;
|
|
esac
|
|
is_valid_integer "$upload_pending_seq" || {
|
|
upload_pending_clear
|
|
return 1
|
|
}
|
|
[ "$upload_pending_seq" -gt 0 ] 2>/dev/null || {
|
|
upload_pending_clear
|
|
return 1
|
|
}
|
|
|
|
payload_generation="$(ss_json_get_file "$SS_STATISTICS_UPLOAD_PENDING_FILE" '@.generation_id')"
|
|
payload_seq="$(ss_json_get_file "$SS_STATISTICS_UPLOAD_PENDING_FILE" '@.snapshot_seq')"
|
|
if [ "$payload_generation" != "$upload_pending_generation" ] || [ "$payload_seq" != "$upload_pending_seq" ]; then
|
|
upload_pending_clear
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
upload_source_is_v3() {
|
|
local file="$1"
|
|
local schema_name schema_version seq generation
|
|
|
|
[ -s "$file" ] || return 1
|
|
schema_name="$(ss_json_get_file "$file" '@.schema.name')"
|
|
schema_version="$(ss_json_get_file "$file" '@.schema.version')"
|
|
generation="$(ss_json_get_file "$file" '@.generation_id')"
|
|
seq="$(ss_json_get_file "$file" '@.snapshot_seq')"
|
|
[ "$schema_name" = 'safeshield.statistics' ] || return 1
|
|
[ "$schema_version" = '3' ] || return 1
|
|
[ -n "$generation" ] || return 1
|
|
is_valid_integer "$seq" || return 1
|
|
[ "$seq" -gt 0 ] 2>/dev/null
|
|
}
|
|
|
|
upload_pending_create() {
|
|
local source mode source_generation source_seq full_generation full_seq
|
|
local tmp_payload tmp_meta
|
|
|
|
upload_state_load
|
|
upload_source_is_v3 "$SS_STATISTICS_JSON_FILE" || return 1
|
|
full_generation="$(ss_json_get_file "$SS_STATISTICS_JSON_FILE" '@.generation_id')"
|
|
full_seq="$(ss_json_get_file "$SS_STATISTICS_JSON_FILE" '@.snapshot_seq')"
|
|
|
|
if [ "$upload_state_generation" != "$full_generation" ]; then
|
|
upload_state_generation="$full_generation"
|
|
upload_state_last_success_seq=0
|
|
upload_state_last_full_seq=0
|
|
upload_state_recent_successes=0
|
|
upload_state_needs_full=1
|
|
upload_state_save >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
mode='recent'
|
|
source="$SS_STATISTICS_UPLOAD_JSON_FILE"
|
|
if [ "$upload_state_needs_full" = '1' ] ||
|
|
[ "$upload_state_last_full_seq" -le 0 ] 2>/dev/null ||
|
|
[ "$upload_state_recent_successes" -ge "$SS_STATS_UPLOAD_FULL_EVERY" ] 2>/dev/null; then
|
|
mode='full'
|
|
source="$SS_STATISTICS_JSON_FILE"
|
|
elif ! upload_source_is_v3 "$source"; then
|
|
mode='full'
|
|
source="$SS_STATISTICS_JSON_FILE"
|
|
fi
|
|
|
|
tmp_payload="${SS_STATISTICS_UPLOAD_PENDING_FILE}.tmp.$$"
|
|
tmp_meta="${SS_STATISTICS_UPLOAD_PENDING_META_FILE}.tmp.$$"
|
|
mkdir -p "${SS_STATISTICS_UPLOAD_PENDING_FILE%/*}" || return 1
|
|
cp "$source" "$tmp_payload" || {
|
|
rm -f "$tmp_payload"
|
|
return 1
|
|
}
|
|
|
|
upload_source_is_v3 "$tmp_payload" || {
|
|
rm -f "$tmp_payload"
|
|
return 1
|
|
}
|
|
source_generation="$(ss_json_get_file "$tmp_payload" '@.generation_id')"
|
|
source_seq="$(ss_json_get_file "$tmp_payload" '@.snapshot_seq')"
|
|
if [ "$source_generation" != "$full_generation" ]; then
|
|
rm -f "$tmp_payload"
|
|
return 1
|
|
fi
|
|
if [ "$mode" = 'recent' ] && [ "$source_seq" != "$full_seq" ]; then
|
|
# The collector publishes the full snapshot before its recent projection.
|
|
# Avoid promoting a previous projection while those two atomic writes cross.
|
|
rm -f "$tmp_payload"
|
|
return 1
|
|
fi
|
|
if [ "$source_seq" -le "$upload_state_last_success_seq" ] 2>/dev/null; then
|
|
rm -f "$tmp_payload"
|
|
return 2
|
|
fi
|
|
|
|
printf '%s\t%s\t%s\n' "$mode" "$source_generation" "$source_seq" >"$tmp_meta" || {
|
|
rm -f "$tmp_payload" "$tmp_meta"
|
|
return 1
|
|
}
|
|
mv -f "$tmp_payload" "$SS_STATISTICS_UPLOAD_PENDING_FILE" || {
|
|
rm -f "$tmp_payload" "$tmp_meta"
|
|
return 1
|
|
}
|
|
mv -f "$tmp_meta" "$SS_STATISTICS_UPLOAD_PENDING_META_FILE" || {
|
|
rm -f "$tmp_meta"
|
|
upload_pending_clear
|
|
return 1
|
|
}
|
|
|
|
upload_pending_mode="$mode"
|
|
upload_pending_generation="$source_generation"
|
|
upload_pending_seq="$source_seq"
|
|
return 0
|
|
}
|
|
|
|
upload_ensure_credentials() {
|
|
ss_statistics_upload_denied && return 2
|
|
ss_statistics_load_upload_credentials && return 0
|
|
log_info 'Refreshing Hub statistics upload credentials'
|
|
ss_statistics_refresh_upload_credentials || return $?
|
|
ss_statistics_load_upload_credentials
|
|
}
|
|
|
|
upload_response_valid() {
|
|
local response="$1"
|
|
local status generation server_seq
|
|
|
|
[ -s "$response" ] || return 1
|
|
status="$(ss_json_get_file "$response" '@.status')"
|
|
generation="$(ss_json_get_file "$response" '@.generation_id')"
|
|
server_seq="$(ss_json_get_file "$response" '@.last_snapshot_seq')"
|
|
case "$status" in
|
|
applied | duplicate | stale) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
[ "$generation" = "$upload_pending_generation" ] || return 1
|
|
is_valid_integer "$server_seq" || return 1
|
|
[ "$server_seq" -ge "$upload_pending_seq" ] 2>/dev/null || return 1
|
|
|
|
SS_STATS_UPLOAD_RESPONSE_STATUS="$status"
|
|
SS_STATS_UPLOAD_RESPONSE_SEQ="$server_seq"
|
|
return 0
|
|
}
|
|
|
|
upload_record_success() {
|
|
local result_status="$1"
|
|
local server_seq="$2"
|
|
|
|
upload_state_load
|
|
if [ "$upload_state_generation" != "$upload_pending_generation" ]; then
|
|
upload_state_generation="$upload_pending_generation"
|
|
upload_state_last_success_seq=0
|
|
upload_state_last_full_seq=0
|
|
upload_state_recent_successes=0
|
|
upload_state_needs_full=1
|
|
fi
|
|
if [ "$server_seq" -gt "$upload_state_last_success_seq" ] 2>/dev/null; then
|
|
upload_state_last_success_seq="$server_seq"
|
|
fi
|
|
|
|
if [ "$upload_pending_mode" = 'full' ] && [ "$result_status" != 'stale' ]; then
|
|
upload_state_last_full_seq="$upload_pending_seq"
|
|
upload_state_recent_successes=0
|
|
upload_state_needs_full=0
|
|
else
|
|
if [ "$result_status" = 'stale' ]; then
|
|
upload_state_needs_full=1
|
|
elif [ "$upload_state_needs_full" = '0' ]; then
|
|
upload_state_recent_successes=$((upload_state_recent_successes + 1))
|
|
fi
|
|
fi
|
|
upload_state_save >/dev/null 2>&1 || true
|
|
}
|
|
|
|
upload_record_failure() {
|
|
upload_mark_needs_full "$upload_pending_generation"
|
|
}
|
|
|
|
upload_send_pending() {
|
|
local retries reauthenticated=0 response_status response_seq credentials_rc=0 refresh_rc=0
|
|
|
|
upload_pending_load || return 1
|
|
upload_ensure_credentials || credentials_rc=$?
|
|
case "$credentials_rc" in
|
|
0) ;;
|
|
2)
|
|
upload_pending_clear
|
|
return 2
|
|
;;
|
|
*)
|
|
upload_record_failure 'statistics_credentials_unavailable'
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
retries=1
|
|
while [ "$retries" -le "${ss_download_retry:-3}" ]; do
|
|
[ "$ss_should_terminate" -eq 1 ] && return 130
|
|
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
|
|
if ss_http_post_json \
|
|
"$SS_STATISTICS_UPLOAD_URL" \
|
|
"$SS_STATISTICS_UPLOAD_PENDING_FILE" \
|
|
"$SS_STATS_UPLOAD_RESPONSE_FILE" \
|
|
"Bearer $SS_STATISTICS_UPLOAD_TOKEN"; then
|
|
if upload_response_valid "$SS_STATS_UPLOAD_RESPONSE_FILE"; then
|
|
response_status="$SS_STATS_UPLOAD_RESPONSE_STATUS"
|
|
response_seq="$SS_STATS_UPLOAD_RESPONSE_SEQ"
|
|
upload_record_success "$response_status" "$response_seq"
|
|
log_info "Uploaded SafeShield statistics (${upload_pending_mode}, seq=${upload_pending_seq}, status=${response_status})"
|
|
upload_pending_clear
|
|
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
|
|
return 0
|
|
fi
|
|
|
|
upload_record_failure 'statistics_upload_invalid_response' '200'
|
|
log_warn 'Hub statistics upload returned an invalid success response; preserving pending payload for retry'
|
|
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
|
|
return 1
|
|
fi
|
|
|
|
case "${SS_HTTP_STATUS:-}" in
|
|
401)
|
|
if [ "$reauthenticated" -eq 0 ]; then
|
|
refresh_rc=0
|
|
reauthenticated=1
|
|
ss_statistics_clear_upload_credentials
|
|
ss_statistics_refresh_upload_credentials || refresh_rc=$?
|
|
if [ "$refresh_rc" -eq 0 ] && ss_statistics_load_upload_credentials; then
|
|
continue
|
|
fi
|
|
if [ "$refresh_rc" -eq 2 ]; then
|
|
log_info 'Cloud statistics upload is no longer enabled for the current account entitlement'
|
|
upload_pending_clear
|
|
return 2
|
|
fi
|
|
fi
|
|
upload_record_failure 'statistics_upload_auth_failed' '401'
|
|
log_warn 'Hub rejected statistics upload credentials'
|
|
return 1
|
|
;;
|
|
400 | 409 | 413)
|
|
upload_record_failure "statistics_upload_http_${SS_HTTP_STATUS}" "$SS_HTTP_STATUS"
|
|
log_error "Hub rejected statistics payload (HTTP ${SS_HTTP_STATUS}); discarding this snapshot and scheduling full reconciliation"
|
|
upload_pending_clear
|
|
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
|
|
return 2
|
|
;;
|
|
esac
|
|
|
|
upload_record_failure 'statistics_upload_failed' "${SS_HTTP_STATUS:-0}"
|
|
retries=$((retries + 1))
|
|
if [ "$retries" -le "${ss_download_retry:-3}" ]; then
|
|
upload_sleep "$SS_STATS_UPLOAD_RETRY_DELAY_S" || return 130
|
|
fi
|
|
done
|
|
|
|
log_warn "Statistics upload failed after ${ss_download_retry:-3} attempt(s); pending payload will be retried"
|
|
return 1
|
|
}
|
|
|
|
main() {
|
|
local create_rc=0 wait_s entitlement_rc
|
|
|
|
ss_load_config || return 1
|
|
[ "${ss_enabled}" = '1' ] || return 0
|
|
[ "${ss_statistics_enabled}" = '1' ] || return 0
|
|
mkdir -p "$SS_STATISTICS_DIR" || return 1
|
|
|
|
is_valid_integer "$SS_STATS_UPLOAD_INTERVAL_S" || SS_STATS_UPLOAD_INTERVAL_S=1800
|
|
[ "$SS_STATS_UPLOAD_INTERVAL_S" -ge 60 ] 2>/dev/null || SS_STATS_UPLOAD_INTERVAL_S=1800
|
|
is_valid_integer "$SS_STATS_UPLOAD_IDLE_WAIT_S" || SS_STATS_UPLOAD_IDLE_WAIT_S=15
|
|
[ "$SS_STATS_UPLOAD_IDLE_WAIT_S" -ge 1 ] 2>/dev/null || SS_STATS_UPLOAD_IDLE_WAIT_S=15
|
|
is_valid_integer "$SS_STATS_UPLOAD_RETRY_DELAY_S" || SS_STATS_UPLOAD_RETRY_DELAY_S=2
|
|
is_valid_integer "$SS_STATS_UPLOAD_FULL_EVERY" || SS_STATS_UPLOAD_FULL_EVERY=23
|
|
[ "$SS_STATS_UPLOAD_FULL_EVERY" -ge 1 ] 2>/dev/null || SS_STATS_UPLOAD_FULL_EVERY=23
|
|
is_valid_integer "$SS_STATS_UPLOAD_DENIED_RECHECK_S" || SS_STATS_UPLOAD_DENIED_RECHECK_S=43200
|
|
[ "$SS_STATS_UPLOAD_DENIED_RECHECK_S" -ge 60 ] 2>/dev/null || SS_STATS_UPLOAD_DENIED_RECHECK_S=43200
|
|
|
|
upload_state_load
|
|
while [ "$ss_should_terminate" -eq 0 ]; do
|
|
ss_load_config || {
|
|
upload_sleep "$SS_STATS_UPLOAD_IDLE_WAIT_S" || break
|
|
continue
|
|
}
|
|
if [ "${ss_enabled}" != '1' ] || [ "${ss_statistics_enabled}" != '1' ]; then
|
|
break
|
|
fi
|
|
|
|
wait_s="$SS_STATS_UPLOAD_INTERVAL_S"
|
|
if ss_statistics_upload_denied; then
|
|
upload_pending_clear
|
|
if ss_statistics_upload_denied_recheck_due "$SS_STATS_UPLOAD_DENIED_RECHECK_S"; then
|
|
log_info 'Rechecking Hub statistics upload entitlement'
|
|
# Stamp this attempt before network I/O so a failed device sync cannot
|
|
# turn into a routine upload retry loop while entitlement is denied.
|
|
ss_statistics_set_upload_entitlement denied >/dev/null 2>&1 || true
|
|
entitlement_rc=0
|
|
ss_statistics_refresh_upload_credentials || entitlement_rc=$?
|
|
case "$entitlement_rc" in
|
|
0) ;;
|
|
2)
|
|
upload_sleep "$wait_s" || break
|
|
continue
|
|
;;
|
|
*)
|
|
log_warn 'Hub statistics entitlement recheck failed; keeping cloud upload disabled'
|
|
upload_sleep "$wait_s" || break
|
|
continue
|
|
;;
|
|
esac
|
|
fi
|
|
if ss_statistics_upload_denied; then
|
|
upload_sleep "$wait_s" || break
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
if upload_pending_load; then
|
|
upload_send_pending || true
|
|
else
|
|
create_rc=0
|
|
upload_pending_create || create_rc=$?
|
|
case "$create_rc" in
|
|
0)
|
|
upload_send_pending || true
|
|
;;
|
|
1)
|
|
wait_s="$SS_STATS_UPLOAD_IDLE_WAIT_S"
|
|
;;
|
|
2)
|
|
# The collector has not produced a sequence newer than the last ACK yet.
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
upload_sleep "$wait_s" || break
|
|
done
|
|
|
|
rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"
|
|
return 0
|
|
}
|
|
|
|
if [ "${SS_STATS_UPLOAD_LIBRARY_ONLY:-0}" != '1' ]; then
|
|
trap 'upload_handle_term' TERM INT
|
|
trap 'rm -f "$SS_STATS_UPLOAD_RESPONSE_FILE"' EXIT
|
|
main "$@"
|
|
fi
|