#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## style-ok: no-strict -- dual-mode: strict is set only inside 'if was_executed' (a direct run, so it cannot leak into a shell that sources this file), hence not at column 0; 'nounset' deliberately absent (script relies on unset vars expanding to empty).

## Exit codes of this script get interpreted by sdwdate.
## exit 0
## exit 1: wait, retry and error icon
## exit 2: wait, retry and busy icon

## provides was_executed
# shellcheck source=./check_runtime.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/check_runtime.bsh

## Dual-mode: this file is also SOURCED (by its testscript / helper contexts), so
## strict mode is enabled on a DIRECT run only and cannot leak into the sourcing
## shell. 'nounset' is deliberately absent: this script relies on unset variables
## expanding to empty.
if was_executed "${BASH_SOURCE[0]}"; then
   set -o errexit
   set -o pipefail
   shopt -s inherit_errexit
   shopt -s shift_verbose
fi

output_cmd() {
   msg="$*"
   printf '%s\n' "__ ${msg}"
}

error_handler() {
   exit_code="$?"
   printf '%s\n' "\
BASH_COMMAND: ${BASH_COMMAND}
exit_code: ${exit_code}"
}

trap "error_handler" ERR

exit_handler() {
   if [ "${exit_code}" = "" ]; then
      output_cmd "$0: No exit code set yet. Setting to 1."
      exit_code=1
   fi
   if [ "${exit_code}" = "0" ]; then
      meaning="success"
   elif [ "${exit_code}" = "1" ]; then
      meaning="wait, show error icon and retry."
   elif [ "${exit_code}" = "2" ]; then
      meaning="wait, show busy icon and retry."
   else
      meaning="Unknown."
   fi
   output_cmd "### END: ### Exiting with exit_code '${exit_code}' indicating '${meaning}'."
   exit "${exit_code}"
}

trap "exit_handler" EXIT

source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/tor_enabled_check

## Not actually used anymore. Might cause:
## Feb 14 02:36:29 host sdwdate[909]: 2025-02-14 02:36:29 - sdwdate - INFO - /usr/libexec/helper-scripts/onion-time-pre-script: WARNING: privleapd is not running. Folder '/proc/681' does not exist. Cannot use privleap.
#source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/pkg_manager_running_check

## provides check_tor_bootstrap_status
# shellcheck source=./tor_bootstrap_check.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/tor_bootstrap_check.bsh

## provides is_whole_number
# shellcheck source=./strings.bsh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/strings.bsh

detect_vm_type() {
   if [ -f "/usr/share/anon-gw-base-files/gateway" ]; then
      VM="Gateway"
   elif [ -f "/usr/share/anon-ws-base-files/workstation" ]; then
      VM="Workstation"
   else
      VM="Other"
   fi
}

anondate_folder() {
   true "user: ${USER}"

   if [ "${USER}" = "sdwdate" ]; then
      anondate_state_folder=/run/sdwdate
   elif [ "$(id -u)" = "0" ]; then
      anondate_state_folder=/run/anondate
   else
      output_cmd "ERROR: running under user '${USER}' is not yet implemented. Run:
sudo -u sdwdate $0"

      anondate_state_folder=/run/anondate

      exit_code=1
      exit "${exit_code}"
   fi

   true "anondate_state_folder: ${anondate_state_folder}"
}

onion_time_script_status() {
   if [ -f "${anondate_state_folder}/onion-time-script-after-boot" ]; then
      output_cmd "Status: Subsequent run after boot."
      onion_time_script_status_boot=true
   else
      output_cmd "Status: First run after boot. (Creating file '${anondate_state_folder}/onion-time-script-after-boot'.)"
      onion_time_script_status_boot=false
      touch "${anondate_state_folder}/onion-time-script-after-boot"
   fi
}

## Set by timesanitycheck_static below, read by tor_consensus_time_sanity_check
## and by the anondate_use branch. Declared here so both reads are defined even
## if the static check never runs.
timesanitycheck_static_timestamp_based_failed=""

timesanitycheck_static() {
   local timesanitycheck_output timesanitycheck_exit_code

   ## The exit code was being DISCARDED by '|| true', so the flag it should set
   ## was never assigned -- and the two things gated on that flag have therefore
   ## never happened: the duplicate-output avoidance in
   ## tor_consensus_time_sanity_check, and the anondate_use clock-recovery
   ## branch further down.
   ##
   ## timesanitycheck exits 0 when the clock is within the shipped min/expiry
   ## window, and 2 or 3 when it is not.
   timesanitycheck_exit_code=0
   timesanitycheck_output="$(timesanitycheck)" || timesanitycheck_exit_code="$?"

   output_cmd "Static Time Sanity Check: ${timesanitycheck_output}"

   if [ "${timesanitycheck_exit_code}" != "0" ]; then
      timesanitycheck_static_timestamp_based_failed="true"
   fi
}

exit_if_tor_not_yet_enabled() {
   ## Sets: TOR_ENABLED
   check_tor_enabled_do

   ## check_tor_enabled_do (tor_enabled_check, sourced) assigns TOR_ENABLED on
   ## its FIRST line as a fallback, so it is always set. shellcheck sees only
   ## this file and cannot know that.
   # shellcheck disable=SC2154
   if [ "${TOR_ENABLED}" = "1" ]; then
      true "Tor enabled check: Tor already enabled, ok."
   else
      if [ -f /usr/share/whonix/marker ]; then
         output_cmd "Tor enabled check: Tor is disabled. Please enable Tor using Anon Connection Wizard or setup-dist. \
Start Menu -> System -> Anon Connection Wizard or in Terminal: sudo setup-dist"
      else
         output_cmd "Tor enabled check: Tor is disabled. Please enable Tor in the Tor config."
      fi

      exit_code=1
      exit "${exit_code}"
   fi
}

tor_dormant_status() {
   ## sets: tor_dormant_status
   check_tor_dormant_status ## tor_bootstrap_check.bsh

   if [ "${tor_dormant_status}" = "0" ]; then
      output_cmd "Tor Dormant Result: No, not dormant, good. (tor_dormant_type: '${tor_dormant_type}')"
   elif [ "${tor_dormant_status}" = "1" ]; then
      output_cmd "Tor Dormant Result: Yes, dormant. (tor_dormant_type: '${tor_dormant_type}') Sending 'SIGNAL active' and 'SIGNAL newnym' to wake up Tor..."
      ## https://stackoverflow.com/questions/37135213/check-tor-connection-is-established-before-running-scrapy
      run_signal_newnym
   else
      output_cmd "Tor Dormant Result: Unknown."
   fi
}

check_tor_circuit_established_yet() {
   ## sets: check_bootstrap_privleap_action
   ## sets: lastpid
   ## sets: tor_bootstrap_percent
   ## sets: tor_bootstrap_status
   check_tor_circuit_established ## tor_bootstrap_check.bsh

   ## $tor_circuit_established_check_exit_code on timeout returns:
   ## - 124 if sigterm was sufficient
   ## - 137 if needed to use kill.

   for invalid_exit_code in "124" "137" "254" ; do
      if [ "${tor_circuit_established_check_exit_code}" = "${invalid_exit_code}" ]; then
         output_cmd "Tor Bootstrap Result:
check_bootstrap_privleap_action: ${check_bootstrap_privleap_action}
tor_bootstrap_timeout_type: ${tor_bootstrap_timeout_type}
tor_circuit_established_check_exit_code: ${tor_circuit_established_check_exit_code}"
         exit_code=1
         exit "${exit_code}"
      fi
   done

   if [ "${tor_circuit_established_check_exit_code}" = "255" ]; then
      if [ "${VM}" = "Gateway" ]; then
         output_cmd "Tor Bootstrap Result: \
Tor's Control Port could not be reached."
      elif [ "${VM}" = "Workstation" ]; then
         if [ -f /usr/share/whonix/marker ]; then
            output_cmd "Tor Bootstrap Result: \
Tor's Control Port could not be reached.\
Did you start Gateway beforehand? \
Please run systemcheck on Gateway."
         else
            output_cmd "Tor Bootstrap Result: \
Tor's Control Port could not be reached.\
Did you start Gateway beforehand?"
         fi
      else
         if [ -f /usr/share/whonix/marker ]; then
            output_cmd "Tor Bootstrap Result: \
Tor's Control Port could not be reached.\
Did you start Gateway beforehand? \
Please run systemcheck on Gateway.
${FUNCNAME[0]}: This is neither a gateway nor a workstation. Please report this bug!"
         else
            output_cmd "Tor Bootstrap Result: \
Tor's Control Port could not be reached."
         fi
      fi

      exit_code=1
      exit "${exit_code}"
   fi
}

## sdwdate's preparation loop lengthens its retry interval only for as long as
## this script's output stays byte-identical between runs. Tor's bootstrap line
## carries COUNT=, a retry counter incremented on every failed connection
## attempt, so on a Gateway without network the output never repeats and the
## retry interval stays pinned at its minimum. Redact the counter so an
## otherwise unchanged status compares equal.
##
## Only the printed copy is redacted. ${tor_bootstrap_status} keeps the counter
## for callers matching on it, and systemcheck still displays the full line.
##
## sets: tor_bootstrap_status_redacted
redact_bootstrap_retry_count() {
   tor_bootstrap_status_redacted="$1"
   while [[ "${tor_bootstrap_status_redacted}" =~ ^(.*)[[:space:]]COUNT=[0-9]+(.*)$ ]]; do
      tor_bootstrap_status_redacted="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
   done
}

tor_bootstrap_check() {
   if [ "${VM}" = "Workstation" ]; then
      true "${FUNCNAME[0]}: INFO: skipping check_tor_bootstrap_status because workstation has no access to it."
   else
      check_tor_bootstrap_status
   fi

   ## TODO:
   ## When using an old Tor consensus which might be the case when no Tor
   ## circuit has been established yet, there is no point to check Tor
   ## consensus time as it might be outdated leading to concluding that the
   ## clock is fast.

   ## Could be 100% bootstrap process but still no Tor circuit established yet.

   if [ "${VM}" = "Workstation" ]; then
      output_cmd "Tor circuit: ${tor_circuit_established_word}"
   else
      ## sets: tor_bootstrap_status_redacted
      redact_bootstrap_retry_count "${tor_bootstrap_status}"
      output_cmd "Tor reports: ${tor_bootstrap_status_redacted}"
      output_cmd "Tor circuit: ${tor_circuit_established_word}"

      if printf '%s\n' "${tor_bootstrap_status}" | grep -- "REASON=TIMEOUT" &>/dev/null; then
         output_cmd "Tor reports: REASON=TIMEOUT"
      fi
   fi
}

tor_consensus_time_sanity_check() {
   ## If the static timestamp based time sanity check failed, there is no
   ## need to run the Tor consensus based time sanity check. Avoiding
   ## duplicate output.
   ## Now genuinely reachable: timesanitycheck_static sets the flag when the
   ## static check fails. Before that wiring nothing assigned it, so this early
   ## return never fired.
   if [ "${timesanitycheck_static_timestamp_based_failed}" = "true" ]; then
      return 0
   fi

   ## sets: tor_consensus_valid_after_exit_code
   ## sets: tor_consensus_valid_after_output
   ## sets: tor_consensus_valid_after_unixtime
   tor_consensus_valid-after

   ## sets: tor_consensus_valid_until_exit_code
   ## sets: tor_consensus_valid_until_output
   ## sets: tor_consensus_valid_until_unixtime
   tor_consensus_valid-until

   current_unixtime="$(date --utc +"%s")"

   if [ ! "${tor_consensus_valid_after_unixtime}" = "" ] && [ ! "${tor_consensus_valid_until_unixtime}" = "" ]; then
      clock_tor_consensus_check_result="ok"
      if [ "${current_unixtime}" -ge "${tor_consensus_valid_after_unixtime}" ]; then
         true
      else
         clock_tor_consensus_check_result="slow"
         clock_tor_consensus_check_msg="The clock might be too slow. Clock is slower than consensus/valid-after ${tor_consensus_valid_after_output}."
      fi
      if [ "${current_unixtime}" -ge "${tor_consensus_valid_until_unixtime}" ]; then
         clock_tor_consensus_check_result="fast"
         if [ "${onion_time_script_status_boot}" = "true" ]; then
            clock_tor_consensus_check_msg="The clock might be too fast. Clock is faster than consensus/valid-until ${tor_consensus_valid_until_output}. \
extra guess: Tor consensus might be outdated and download might still be in progress since this is the first run after boot."
         else
            clock_tor_consensus_check_msg="The clock might be too fast. Clock is faster than consensus/valid-until ${tor_consensus_valid_until_output}. extra guess: None."
         fi
      else
         true
      fi
   elif [ "${tor_consensus_valid_after_exit_code}" = "277" ] && [ "${tor_consensus_valid_until_exit_code}" = "277" ]; then
      clock_tor_consensus_check_result="noneyet"
      clock_tor_consensus_check_msg="Might not have downloaded a Tor consensus yet."
   else
      clock_tor_consensus_check_result="error"
      clock_tor_consensus_check_msg="Consensus time sanity check failed."
   fi

   if [ "${clock_tor_consensus_check_result}" = "ok" ]; then
      clock_tor_consensus_check_result="ok"
      clock_tor_consensus_check_msg="Clock within consensus parameters consensus/valid-after '${tor_consensus_valid_after_output}' and consensus/valid-until '${tor_consensus_valid_until_output}'."
   fi

   if [ "${clock_tor_consensus_check_result}" = "ok" ]; then
      output_cmd "Tor Consensus Time Sanity Check: ${clock_tor_consensus_check_msg}"
   else
      output_cmd "Tor Consensus Time Sanity Check: ${clock_tor_consensus_check_msg}"
   fi

   ## TODO
   ## Would have to parse tor_bootstrap_status.
   ## In case Tor cannot fetch Tor consensus $tor_consensus_valid_after_exit_code /
   ## $tor_consensus_valid_until_exit_code may be zero but $tor_consensus_valid_until_output
   ## may be empty.
   #if [ ! "$clock_tor_consensus_check_result" = "ok" ]; then
   #   if [ "$VM" = "Gateway" ]; then
   #      exit_code=1
   #      exit "$exit_code"
   #   fi
   #fi
}

connectivity_test() {
    ## TODO: optional connectivity test
    #/usr/bin/url_to_unixtime 127.0.0.1 9050 http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion 80 true 2>&1
    true "connectivity_test: Not yet implemented."
}

exit_success_if_tor_circuit_already_established() {
   if [ "${tor_circuit_established}" = "1" ]; then
      output_cmd "Conclusion: Tor already reports circuit established."
      exit_code=0
      exit "${exit_code}"
   fi

   ## https://stackoverflow.com/questions/37135213/check-tor-connection-is-established-before-running-scrapy

   if [ "${tor_bootstrap_percent}" = "100" ]; then
      output_cmd "Conclusion:  No Tor circuit established yet, but Tor bootstrap progress (tor_bootstrap_percent) is 100."

      exit_code=0
      exit "${exit_code}"
   fi

   ## Happened that Tor bootstrap was 100% done but not circuit established.
   ## Example Tor log:
   ## Bootstrapped 100% (done): Done
   ## Our directory information is no longer up-to-date enough to build circuits: We're missing descriptors for 1/2 of our primary entry guards (total microdescriptors: 6967/6984). That's ok. We will try to fetch missing descriptors soon.
   output_cmd "Conclusion: No Tor circuit established yet."
}

## Minimum spacing between two 'anondate-set' requests.
##
## Nothing 'anondate-set' consumes moves faster than this. It derives the time
## from the Tor consensus (published hourly, valid three hours) or from the Tor
## certificate lifetime, so re-requesting more often cannot produce a different
## answer.
##
## sdwdate's preparation loop runs onion-time-pre-script roughly once per
## second while Tor is unreachable, and
## sdwdate-start-anondate-set-file-watcher has no rate limit, so if this script
## ran anondate-set unconditionally, it would waste a lot of resources.
anondate_request_min_interval_seconds="60"

anondate_request_rate_limited() {
   ## Returns 0 when the previous request is younger than
   ## anondate_request_min_interval_seconds, meaning the caller should not
   ## create another one.
   local stamp_file stamp_unixtime current_unixtime request_age_seconds

   stamp_file="${anondate_state_folder}/request_anondate-set.stamp"

   if [ ! -f "${stamp_file}" ]; then
      return 1
   fi

   stamp_unixtime="$(date --utc --reference="${stamp_file}" "+%s")" || return 1
   is_whole_number "${stamp_unixtime}" || return 1
   current_unixtime="$(date --utc "+%s")" || return 1
   is_whole_number "${current_unixtime}" || return 1

   request_age_seconds="$((current_unixtime - stamp_unixtime))"

   ## A negative age means the clock moved backwards since the stamp was
   ## written, so the stamp is stale and the rate limit must not apply.
   ## anondate-set is forward-only by design (it refuses to set the clock to an
   ## earlier time), so it never causes this; sdwdate can correct the clock
   ## forward OR backward, and manual/NTP/VM-snapshot changes can too, which is
   ## what this guard covers.
   if [ "${request_age_seconds}" -lt 0 ]; then
      return 1
   fi

   if [ "${request_age_seconds}" -ge "${anondate_request_min_interval_seconds}" ]; then
      return 1
   fi

   return 0
}

anondate_use() {
   if [ "${VM}" = "Workstation" ]; then
      output_cmd "anondate_use: Skipping 'anondate-set' since this is a workstation, ok."
      return 0
   fi

   if anondate_request_rate_limited; then
      output_cmd "anondate_use: Skipping 'anondate-set' request, the previous request is less than ${anondate_request_min_interval_seconds} seconds old, ok."
      exit_code=2
      exit "${exit_code}"
   fi

   ## https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
   ## https://www.whonix.org/wiki/Dev/TimeSync#Fixing_Time_based_on_Tor_Consensus
   ## /usr/libexec/sdwdate/sdwdate-start-anondate-set-file-watcher
   output_cmd "anondate_use: Running 'anondate-set' (by creating file '${anondate_state_folder}/request_anondate-set')..."

   touch "${anondate_state_folder}/request_anondate-set.stamp"
   touch "${anondate_state_folder}/request_anondate-set"

   exit_code=2
   exit "${exit_code}"
}

determine_circuit_confirmed() {
   ## Sets circuit_confirmed=1 when a real circuit is confirmed. Gateway:
   ## actively build a fresh circuit (non-stale, unlike the sticky
   ## status/circuit-established flag). Workstation: onion-grater forbids
   ## EXTENDCIRCUIT, and it defers to the gateway, so fall back to the flag.
   if [ "${VM}" = "Workstation" ]; then
      circuit_confirmed="${tor_circuit_established}"
      output_cmd "Circuit confirmation (workstation): status/circuit-established = '${tor_circuit_established}'."
      return 0
   fi

   check_tor_circuit_built ## tor_bootstrap_check.bsh

   ## Not outputting $tor_circuit_built_output to avoid minor information leak.
   if [ "${tor_circuit_built_check_exit_code}" = "0" ]; then
      circuit_confirmed="1"
      output_cmd "Circuit confirmation: yes"
   else
      circuit_confirmed="0"
      output_cmd "Circuit confirmation: no"
   fi
}

circuit_confirmed_backward_proceed() {
   ## A confirmed circuit proves the clock is within Tor's tolerance => let
   ## sdwdate fetch and correct it (forward OR backward), regardless of the
   ## consensus verdict. A false positive is safe: sdwdate's fetch is the
   ## real probe (pool median, replay-floor gated) and just retries on bad
   ## times. anondate stays forward-only. Pure function for unit-testing.
   [ "${circuit_confirmed}" = "1" ] || return 1
   return 0
}

te_pe_tb_check() {
   ## For better look inside sdwdate log viewer.
   printf '%s\n' ""

   output_cmd "### START: ### $0"

   anondate_folder

   detect_vm_type

   onion_time_script_status

   timesanitycheck_static

   exit_if_tor_not_yet_enabled

   tor_dormant_status

   check_tor_circuit_established_yet

   tor_bootstrap_check

   tor_consensus_time_sanity_check

   connectivity_test

   ## Cannot use this at this point. While seeing the following log:
   ##
   ## 2021-02-26 14:21:15 - sdwdate - INFO - PREPARATION:
   ## 2021-02-26 14:21:15 - sdwdate - INFO - /usr/libexec/helper-scripts/onion-time-pre-script: Start.
   ## Within minimum time Sun Jan 17 00:00:00 UTC 2021 and expiration timestamp Tue May 17 10:00:00 UTC 2033.
   ## Tor reports: NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
   ## Tor circuit: established.
   ## Tor Consensus Time Sanity Check: The clock might be too fast. Clock is faster than consensus/valid-until 2021-02-26 01:00:00.
   ## Tor already reports circuit established.
   ## /usr/libexec/helper-scripts/onion-time-pre-script: END: Exiting with exit_code '0' indicating 'success'.
   ## 2021-02-26 14:21:15 - sdwdate - INFO - PREPARATION RESULT: SUCCESS.
   ##
   ## Tor reporting "circuit established" by itself is not a reliable determination,
   ## if onion connections can be established. Some Tor onion connections where still timing out.
   ## This happens when turning off Whonix-Gateway for example for one day.
   ## Tor still uses the old Tor consensus until it downloads the new Tor consensus.
   ## Therefore Tor Consensus Time Sanity Check will show in clock being too fast,
   ## which resulted in testing in onion time sources to be mistakenly considered
   ## too fast, while these where actually correct.
   #exit_success_if_tor_circuit_already_established

   if [ "${timesanitycheck_static_timestamp_based_failed}" = "true" ]; then
      anondate_use
      ## Time incorrect by a lot. Indicate error.
      ## [1] Since anondate_use has its own `exit`, no need to use `exit` here.
      ## [2] Do not use `anondate-set` on Whonix-Workstation.
      ##     Since sdwdate can establish onion connections irrespective of Whonix-Workstation system clock,
      ##     as long as Whonix-Gateway Tor is functional.
      #exit_code=1
      #exit "$exit_code"
   fi

   determine_circuit_confirmed
   if circuit_confirmed_backward_proceed; then
      ## Tor circuit confirmed; clock within Tor's tolerance.
      ## Proceeding so sdwdate can fetch the time and correct it.
      output_cmd "Conclusion: Tor already reports circuit confirmed."
      exit_code=0
      exit "${exit_code}"
   fi

   if [ ! "${clock_tor_consensus_check_result}" = "ok" ]; then
      anondate_use
      ## Time incorrect but not by giant amounts.
      ## Probably non-fatal issue. Indicate wait.
      ## [1]
      ## [2]
      #exit_code=2
      #exit "$exit_code"
   fi

   exit_success_if_tor_circuit_already_established

   exit_code=2
   exit "${exit_code}"
}

## Only auto-run when executed, not when sourced (unit tests source it).
if was_executed "${BASH_SOURCE[0]}"; then
   te_pe_tb_check "$@"
fi
