#!/bin/bash

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

check_tor_socks_or_trans_port() {
   local CURL_PROXY_SYSTEMCHECK random_socks_user_name

   if [ ! "$leak_tests" = "true" ]; then
      local MSG="\
<p>$FUNCNAME $1: Skipped, because not using <code>--leak-tests</code> (<code>--show-ip</code>), ok.</p>"
      if [ "$verbose" -ge "1" ]; then
         $output_x ${output_opts[@]} --messagex --typex "info" --message "$MSG"
         $output_cli ${output_opts[@]} --messagecli --typecli "info" --message "$MSG"
      fi
      return 0
   fi

   ## debugging hints
   #CURL=echo
   #TEMP_DIR=/home/user

   if [ "$silent" -ge "3" ]; then
      true "silent is $silent. Skipping FUNCNAME $1."
      return 0
   fi

   ## Do not test for stream isolation if Tor detection failed.
   ## This variable may be set to 1, during the first run of this function.
   if [ "$NOT_USING_TOR" = "1" ]; then
      local MSG="<p><b>$test_name: Not testing Tor's <code>TransPort</code>, because Tor could not be detected previously.</b></p>"
      $output_cli ${output_opts[@]} --messagecli --typecli "info" --message "$MSG"
      return 0
   fi

   local LINK="https://check.torproject.org/api/ip"
   local check_result_curl_exit_code="0"

   local SOCKS_PORT_SYSTEMCHECK="9110"
   local test_word
   if [ "$VM" = "machine" ]; then
      SOCKS_PORT_SYSTEMCHECK="9050"
      test_word="Connectivity Test"
   else
      SOCKS_PORT_SYSTEMCHECK="9110"
      test_word="IP Leak Test"
   fi

   if [ "$1" = "SocksPort" ]; then
      local test_name="<code>SocksPort</code> $test_word"
      local checking_msg="$test_name: Testing Tor's <code>SocksPort</code> (SOCKS_PORT_SYSTEMCHECK: <code>$SOCKS_PORT_SYSTEMCHECK</code>)..."
      ## https://spec.torproject.org/socks-extensions.html
      random_socks_password="$(random_alpha_numeric 43)"
      CURL_PROXY_SYSTEMCHECK=(
         '--proxy' "socks5h://$GATEWAY_IP:$SOCKS_PORT_SYSTEMCHECK"
         '--proxy-user' "<torS0X>0:systemcheck_${random_socks_password}"
      )
      local CHECK_TOR_OUT_FILE="check_tpo_tor_socksport.html"
      if test -f /run/qubes/this-is-templatevm ; then
         local type=info
         local connection_failed_msg="\
<p><code>SocksPort</code> Test Result: Ok, Tor's <code>SocksPort</code> was not reachable.
<br></br>This is expected in default configuration of TemplateVMs in Qubes R4.0 and above, see <a href=${PROJECT_HOMEPAGE}/wiki/Dev/Qubes#Network_in_TemplateVMs>${PROJECT_HOMEPAGE}/wiki/Dev/Qubes#Network_in_TemplateVMs</a>.</p>"
      elif [ "$VM" = "Whonix-Gateway" ] || [ "$VM" = "Whonix-Workstation" ]; then
         local connection_failed_msg="\
<p>$test_name Result: <a href=https://check.torproject.org/api/ip>https://check.torproject.org/api/ip</a> was not reachable.
<br></br>You could check, if you can reach <a href=https://check.torproject.org/api/ip>https://check.torproject.org/api/ip</a> via the Tor Browser Bundle.</p>"
      elif [ "$VM" = "machine" ]; then
         local connection_failed_msg="\
<p>$test_name Result: <a href=https://check.torproject.org/api/ip>https://check.torproject.org/api/ip</a> was not reachable.
<br></br>You could check, if you can reach <a href=https://check.torproject.org/api/ip>https://check.torproject.org/api/ip</a> via the Tor Browser Bundle.</p>"
      else
         local connection_failed_msg="$FUNCNAME: <p>This is neither Whonix-Gateway nor Whonix-Workstation. Please report this bug to the $PROJECT_NAME developers!</p>"
      fi
   elif [ "$1" = "UpdatesProxy" ]; then
      local test_name="<code>UpdatesProxy</code> $test_word"
      local checking_msg="$test_name: Testing Qubes' <code>UpdatesProxy</code>..."
      CURL_PROXY_SYSTEMCHECK=(
         '--proxy' "$PROXY_SERVER"
      )
      local CHECK_TOR_OUT_FILE="check_tpo_tor_transport.html"
      local connection_failed_msg="\
<p><code>UpdatesProxy</code> $test_word: Qubes' <code>UpdatesProxy</code> was not reachable.</p>"
   elif [ "$1" = "TransPort" ]; then
      local test_name="system default networking $test_word"
      local checking_msg="$test_name: ${test_name}..."
      CURL_PROXY_SYSTEMCHECK=()
      local CHECK_TOR_OUT_FILE="check_updates_proxy.html"

      if test -f /run/qubes/this-is-templatevm ; then
         local type=info
         local connection_failed_msg="\
<p>$test_name Test Result: Ok, Tor's <code>TransPort</code> was not reachable.
<br></br>This is expected in default configuration of TemplateVMs in Qubes R4.0 and above, see <a href=${PROJECT_HOMEPAGE}/wiki/Dev/Qubes#Network_in_TemplateVMs>${PROJECT_HOMEPAGE}/wiki/Dev/Qubes#Network_in_TemplateVMs</a>.</p>"
      elif [ "$VM" = "Whonix-Gateway" ]; then
         local type=info
         local connection_failed_msg="\
<p>$test_name Test Result: Ok, Tor's <code>TransPort</code> was not reachable.
<br></br>This is expected on Whonix-Gateway's default configuration.</p>"

      elif [ "$VM" = "machine" ]; then
         local connection_failed_msg="\
<p>$test_name Test Result: Ok, Tor's <code>TransPort</code> was not reachable.</p>"

      else
         local type=error
         local connection_failed_msg="\
<p>$test_name Test Result: Tor's <code>TransPort</code> was not reachable.
<br></br>It is normal for this error to temporarily occur.</p>"
      fi
   else
      local MSG="<p><code>TransPort</code>/<code>SocksPort</code>/<code>UpdatesProxy</code> Test Error: <b>\<code>$1</code> is neither <code>TransPort</code> nor <code>SocksPort</code> nor <code>UpdatesProxy</code>.</b> Please report this bug to the $PROJECT_NAME developers!</p>"
      $output_x ${output_opts[@]} --messagex --typex "error" --message "$MSG"
      $output_cli ${output_opts[@]} --messagecli --typecli "error" --message "$MSG"
      EXIT_CODE="1"
      return 0
   fi

   local MSG="$checking_msg"
   $output_cli ${output_opts[@]} --messagecli --typecli "info" --message "$MSG"

   $CURL \
      --fail \
      $CURL_VERBOSE \
      "${CURL_PROXY_SYSTEMCHECK[@]}" \
      --tlsv1.3 \
      --proto =https \
      --retry-connrefused \
      --retry 3 \
      --retry-delay 3 \
      --retry-max-time 120 \
      --max-time 180 \
      $CURL_TPO_PIN_CERT \
      --output "$TEMP_DIR/$CHECK_TOR_OUT_FILE" \
      -- \
      "$LINK" \
      &

   lastpid="$!"
   wait -- "$lastpid" || { check_result_curl_exit_code="$?" ; true; };

   if [ ! "$check_result_curl_exit_code" = "0" ]; then
      local curl_status_message
      curl_status_message="$(/usr/libexec/helper-scripts/curl_exit_codes "$check_result_curl_exit_code")"

      local MSG="$connection_failed_msg
(curl exit code: $curl_status_message)</p>"
      if [ "$type" = "" ]; then
         type=error
         EXIT_CODE="1"
      fi
      $output_x ${output_opts[@]} --messagex --typex "$type" --message "$MSG"
      $output_cli ${output_opts[@]} --messagecli --typecli "$type" --message "$MSG"
      return 0
   fi

   local bug_maybe_msg
   bug_maybe_msg="Could be a $PROJECT_NAME bug, check.torproject.org change, bug or compromise. Please report this bug to the $PROJECT_NAME developers!"

   ## grep exit codes:
   ## 0 found
   ## 1 otherwise
   ## 2 if an error occurred

   local tor_detected_raw check_tor_out_file_content_raw
   tor_detected_raw="Unknown"

   local max_string_length
   max_string_length=68

   check_tor_out_file_content_raw="$(cat -- "$TEMP_DIR/$CHECK_TOR_OUT_FILE")"
   ## example check_tor_out_file_content_raw - in practice:
   # {"IsTor":true,"IP":"5.255.99.5"}
   ## example check_tor_out_file_content_raw - in theory:
   # {"IsTor":true,"IP":"0000:0000:0000:0000:0000:ffff:192.168.100.228"}
   ## Manual testing.
   #check_tor_out_file_content_raw='{"IsTor":true,"IP":"0000:0000:0000:0000:0000:ffff:192.168.100.228"}'

   local check_tor_out_file_content_sanitized
   check_tor_out_file_content_sanitized="$(/usr/bin/sanitize-string -- "$max_string_length" "$check_tor_out_file_content_raw")"

   if [ "$verbose" -ge "2" ]; then
      local MSG="\
<p>$FUNCNAME $1: <code>check_tor_out_file_content_sanitized:</code>
<blockquote>$check_tor_out_file_content_sanitized</blockquote></p>"
      $output_x ${output_opts[@]} --messagex --typex "info" --message "$MSG"
      $output_cli ${output_opts[@]} --messagecli --typecli "info" --message "$MSG"
   fi

   tor_detected_raw="$(printf "%s" "$check_tor_out_file_content_sanitized" | python3 -c "import sys, json; print(json.load(sys.stdin)['IsTor'])")" || { tor_detected_raw="$?" ; true; };
   ## example tor_detected_raw:
   # True
   ## example tor_detected_raw:
   # False
   ## example output of python3 in case of an error:
   #Traceback (most recent call last):
   #  File "<string>", line 1, in <module>
   #KeyError: 'name'

   max_string_length="5"
   local tor_detected_sanitized
   tor_detected_sanitized="$(/usr/bin/sanitize-string -- "$max_string_length" "$tor_detected_raw")"

   if [ ! "$tor_detected_sanitized" = "False" ]; then
      if [ ! "$tor_detected_sanitized" = "True" ]; then
         ## an error occurred
         local MSG="<p>$test_name Result: tor_detected_sanitized is neither True nor False. $bug_maybe_msg</p>"
         $output_x ${output_opts[@]} --messagex --typex "error" --message "$MSG"
         $output_cli ${output_opts[@]} --messagecli --typecli "error" --message "$MSG"
         EXIT_CODE="1"
         return 0
      fi
   fi

   local json_ip_exit_code ip_raw
   json_ip_exit_code="0"
   ip_raw="$(printf "%s" "$check_tor_out_file_content_sanitized" | python3 -c "import sys, json; print(json.load(sys.stdin)['IP'])")" || { json_ip_exit_code="$?" ; true; };

   ## example ip_raw:
   ## 94.242.204.74

   if [ ! "$json_ip_exit_code" = "0" ]; then
      ## an error occurred
      local MSG="<p>$test_name Result: <b>json_ip_exit_code: $json_ip_exit_code.</b> $bug_maybe_msg</p>"
      $output_x ${output_opts[@]} --messagex --typex "error" --message "$MSG"
      $output_cli ${output_opts[@]} --messagecli --typecli "error" --message "$MSG"
      EXIT_CODE="1"
      return 0
   fi

   ## Test excessive string length.
   #ip_raw="Your IP address appears to be: <b>2001:0db8:85a3:0042:1000:8a2e:0370:7334 \$f(do) \`this\` ... xxx ... xxx </b><br>"

   ## An IPv6 IP is 45 characters long.
   ## https://stackoverflow.com/questions/166132/maximum-length-of-the-textual-representation-of-an-ipv6-address
   max_string_length=45

   local actual_string_length
   actual_string_length="${#ip}"

   if [ "$actual_string_length" -gt "$max_string_length" ]; then
      local MSG="<p><b>$1 Test: Excessive string length of ip_raw variable</b> (<code>$actual_string_length</code> characters).</p>"
      $output_x ${output_opts[@]} --messagex --typex "warning" --message "$MSG"
      $output_cli ${output_opts[@]} --messagecli --typecli "warning" --message "$MSG"
      EXIT_CODE="1"
      return 0
   fi

   local ip_sanitized
   ip_sanitized="$(/usr/bin/sanitize-string -- "$max_string_length" "$ip_raw")"

   ## Store IP for later use in memory.
   if [ "$1" = "SocksPort" ]; then
      IP_SOCKS_PORT="$ip_sanitized"
   elif [ "$1" = "UpdatesProxy" ]; then
      IP_UPDATES_PROXY="$ip_sanitized"
   elif [ "$1" = "TransPort" ]; then
      IP_TRANS_PORT="$ip_sanitized"
   fi

   local ip_show_maybe_text
   if [ "$show_ip" = "true" ]; then
      ip_show_maybe_text="<br></br>IP: <code>$ip_sanitized</code>"
   else
      ip_show_maybe_text=""
   fi

   ## Check if connected to Tor or not,
   ## and choose which message to show.
   if [ "$tor_detected_sanitized" = "True" ]; then
      ## Tor detected.

      if [ "$1" = "SocksPort" ]; then
         CHECK_TOR_RESULT_SOCKS_PORT="$tor_detected_sanitized"
      elif [ "$1" = "UpdatesProxy" ]; then
         CHECK_TOR_RESULT_UPDATES_PROXY="$tor_detected_sanitized"
      elif [ "$1" = "TransPort" ]; then
         CHECK_TOR_RESULT_TRANS_PORT="$tor_detected_sanitized"
      fi

      local MSG="<p>$test_name: Connected to Tor. $ip_show_maybe_text</p>"
      if [ "$silent" -le "0" ]; then
         $output_x ${output_opts[@]} --messagex --typex "info" --message "$MSG"
         $output_cli ${output_opts[@]} --messagecli --typecli "info" --message "$MSG"
      fi
      return 0
   fi

   ## Tor not detected.
   NOT_USING_TOR="1"

   exornator_msg="You could try to find out if this IP is/was a Tor exit node using a search engine or ExoneraTor:
<br></br><a href=https://metrics.torproject.org/exonerator.html>https://metrics.torproject.org/exonerator.html</a>"

   if [ "$VM" = "machine" ]; then
      local not_using_msg_tor="\
<p>$test_name Result: Success. $ip_show_maybe_text</p>"
   else
      local not_using_msg_tor="\
<p>$test_name Result:
<br></br><b>Looks like you are not connected through Tor!</b>
$ip_show_maybe_text</p>

<p>systemcheck aborted!</p>

<p>Possible reasons:
<br></br>- There could be something wrong.
<br></br>- It's a false positive. <a href=https://check.torproject.org/api/ip>https://check.torproject.org/api/ip</a> fails in some cases to detect exit nodes.</p>"
   fi

   local type
   if [ "$VM" = "machine" ]; then
      type=info
   else
      type=error
      EXIT_CODE="1"
      if [ "$1" = "TransPort" ]; then
         local not_using_msg_tor="$not_using_msg_tor

<p>Other in this case common reasons:
<br></br>- You added a VPN.
<br></br>- You added a transproxy.</p>"
      fi

      not_using_msg_tor="$not_using_msg_tor

<p>$exornator_msg</p>"
   fi

   local MSG="$not_using_msg_tor"

   $output_x ${output_opts[@]} --messagex --typex "$type" --message "$MSG"
   $output_cli ${output_opts[@]} --messagecli --typecli "$type" --message "$MSG"

   if [ "$VM" = "machine" ]; then
      true
   else
      EXIT_CODE="1"
      cleanup "1"
   fi

   return 0
}

check_qubes_updates_proxy() {
   if [ ! "$qubes_detected" = "true" ]; then
      return 0
   fi

   ## Abort for any Qubes VM type other than TemplateVM, because
   ## sys-whonix can reach non-torified Qubes UpdatesProxy
   ## https://github.com/QubesOS/qubes-issues/issues/3201
   if ! test -f /run/qubes/this-is-templatevm ; then
      return 0
   fi

   check_tor_socks_or_trans_port "UpdatesProxy"
}

check_tor_socks_port() {
   check_tor_socks_or_trans_port "SocksPort"
}

check_tor_trans_port() {
   check_tor_socks_or_trans_port "TransPort"
}

check_tor_connectivity() {
   ## Uses the network.
   systemcheck_run_function check_tor_socks_port ## check_tor_socks_or_trans_port

   ## Uses the network.
   systemcheck_run_function check_tor_trans_port ## check_tor_socks_or_trans_port

   ## Uses the network.
   systemcheck_run_function check_qubes_updates_proxy ## check_tor_socks_or_trans_port
}
