#!/bin/bash

## Copyright (C) 2021 - 2022 nyxnor <nyxnor@protonmail.com>
## Copyright (C) 2013 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## FIXME: Fix the automated shellcheck tests so that helper-scripts is
## available when shellcheck is run. Do not add back a shellcheck override that
## assumes helper-scripts is missing.

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

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

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

# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/get_colors.sh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/get_colors.sh

## get_colors.sh decides by testing fd 2 (stderr) -- see its '! test -t 2'. This
## script writes the palette to STDOUT, so a redirected report or a pipe into
## another program would receive ANSI bytes whenever stderr merely happened to
## be a terminal. Gate on the stream the colours actually land on.
##
## This does not reimplement the palette: it still comes from get_colors.sh.
## It applies the same tty rule that file applies, to the correct descriptor.
##
## FIXME: Let's make get_colors.sh provide blank color variables if either
## stdout OR stderr is redirected.
if [ ! -t 1 ]; then
   bold=""
   nocolor=""
fi


me="${0##*/}"

tor_control_socket=""
tor_password=""
machine_mode=""
no_circuit=""
client_filter=""
temp_dir=""
stream_target_orig=""
stream_target_clean=""
stream_purpose=""
stream_client=""

error_msg(){
  printf '%s\n' "${me}: [error] ${1}" >&2
  exit 1
}

usage(){
  printf '%s\n' "usage: ${me} [-mzh] [-s socket] [-p password]

  -s [socket]    tor's control socket
                 notice: tcp: [addr:]port: 9051, 127.0.0.1:9051
                 notice: unix: [unix:]path: /run/tor/control,
                         unix:/run/tor/control
                 default: 9051

  -p [pwd]       use password [pwd] instead of Tor's control_auth_cookie
                 default: not used

  -z             don't print circuit's path
                 default: not set

  -m             machine mode
                 notice: script informational and warning messages won't be
                         printed to stdout
                 default: not set

  -a [addr]      filter to only print streams from client addresses matching
                 specified address
                 notice: tcp: addr: 127.0.0.1, 10.137.0.10, 192.168.0.10
                 default: all addresses

  -h             print this help message
"
  exit 1
}

exit_handler() {
  trap "" EXIT
  if test -d "${temp_dir}" ; then
    safe-rm -r -f -- "${temp_dir}"
  fi
  return 0
}

list_streams(){
  ## https://stackoverflow.com/a/22644006 and https://stackoverflow.com/a/53714583
  # shellcheck disable=SC2154
  trap "exit" INT QUIT TERM
  trap "exit_handler" EXIT

  listen_stream="$(cat -- "${temp_dir}/.stream.tmp")"
  for stream_ordered in $(printf '%s\n' "${listen_stream}" | grep -- "^650 STREAM" | cut -d " " -f3 | sort -u | grep -v -- "^[[:space:]]*$" | grep -v -- "^250" || true); do
    printf '%s\n' "${listen_stream}" | grep -- "^650 STREAM ${stream_ordered}" | while IFS=$'\n' read -r stream_line; do
      IFS=" " read -r _ _ stream_id stream_status circuit_id stream_target _ <<-EOF
        $(printf '%s' "${stream_line}")
EOF
      case "${stream_status}" in
        NEW|NEWRESOLVE)
          stream_target_orig="${stream_target}"
          stream_purpose="$(printf '%s\n' "${stream_line}" | tr " " "\n" | grep -- "PURPOSE=" | sed "s/PURPOSE=//")" || true
          stream_client="$(printf '%s\n' "${stream_line}" | tr " " "\n" | grep -- "SOURCE_ADDR=" | sed "s/SOURCE_ADDR=//")" || true
	  test -n "${client_filter}" && [ "${client_filter%:*}" != "${stream_client%:*}" ] && break
        ;;
        REMAP)
          stream_target_cache="$(printf '%s\n' "$(cat -- "${temp_dir}/.stream.hosts" 2>/dev/null) ${stream_target_orig%:*}=${stream_target%:*}")"
          printf '%s\n' "${stream_target_cache}" | tee -- "${temp_dir}/.stream.hosts" >/dev/null
          stream_target_hostname="$(printf '%s\n' "${stream_target_cache}" | tr " " "\n" | grep -F -- "=${stream_target%:*}" | head -n 1 | sed "s|=.*||")" || true
          stream_target_clean="$(printf '%s\n' "${stream_target_hostname}~(${stream_target})")"
        ;;
        CLOSED|SUCCEEDED)
          [ -n "${stream_target_clean}" ] && stream_target="${stream_target_clean}"
          # shellcheck disable=SC2086
          circuit_all="$(tor-ctrl -c "GETINFO circuit-status" ${cli_args})"
          circuit="$(printf '%s\n' "${circuit_all}" | grep -- "^${circuit_id} " | sed "/250 OK/d;/250+circuit-status=/d;/250 closing connection/d")" || true
          circuit_status="$(printf '%s\n' "${circuit}" | cut -d " " -f2)"
          circuit_path="$(printf '%s\n' "${circuit}" | cut -d " " -f3)"
          circuit_purpose="$(printf '%s\n' "${circuit}" | cut -d " " -f5 | sed "s/PURPOSE=//")"
          [ -z "${circuit_status}" ] && break
          if [ -z "${no_circuit}" ]; then
            printf "%s\n" "${bold}------------------------------------------------------------------------------------------------------${nocolor}"
            printf "%s\n" "${bold}Target:${nocolor} ${stream_target}${nocolor}"
            printf "%s\n" "${bold}Stream:${nocolor} ${stream_id}, ${bold}Purpose:${nocolor} ${stream_purpose}, ${bold}Client:${nocolor} ${stream_client}${nocolor}"
            printf "%s\n" "${bold}Circuit:${nocolor} ${circuit_id}, ${bold}Purpose:${nocolor} ${circuit_purpose}${nocolor}"
            printf "%s\n" "${bold}"
            printf '%1s. %-40s %-15s %-19s %-4s %9s\n' "n" "Fingerprint" "Address" "Nickname" "Geo" "Bandwidth"
            printf "%s\n" "${bold}------------------------------------------------------------------------------------------------------${nocolor}"
            hop=0
            for relay in $(printf '%s\n' "${circuit_path}" | tr "," " "); do
              hop=$((hop+1))
              [ "${hop}" -gt 3 ] && break
              relay="$(printf '%s\n' "${relay}" | tr -d "$" | tr "~" " ")"
              relay_fingerprint="$(printf '%s\n' "${relay}" | cut -d " " -f1)"
              relay_nickname="$(printf '%s\n' "${relay}" | cut -d " " -f2)"
              # shellcheck disable=SC2086
              relay_status="$(tor-ctrl -c "GETINFO ns/id/${relay_fingerprint}" ${cli_args})"
              relay_host="$(printf '%s\n' "${relay_status}" | grep -- "^r " | cut -d " " -f7)" || true
              relay_bandwidth="$(printf '%s\n' "${relay_status}" | grep -- "^w " | sed "s/w Bandwidth=//;s/\\r//")" || true
              if ! is_whole_number "${relay_bandwidth}"; then
                relay_bandwidth=0
              fi
              relay_bandwidth="$((relay_bandwidth*512/1024/1024)) MiB/s"
              # shellcheck disable=SC2086
              relay_locale="$(tor-ctrl -c "GETINFO ip-to-country/${relay_host}" ${cli_args})"
              relay_locale="$(printf '%s\n' "${relay_locale}" | grep -F -- "250-ip-to-country/${relay_host}=" | sed "s/.*=//;s/\\r//")" || true
              printf '%1s. %-40s %-15s %-19s %-4s %9s\n' "${hop}" "${relay_fingerprint}" "${relay_host}" "${relay_nickname}" "(${relay_locale})" "${relay_bandwidth}"
            done
            printf "%s\n" "${bold}------------------------------------------------------------------------------------------------------${nocolor}"
          else
            ! test -f "${temp_dir}/.stream.loop" && {
              touch -- "${temp_dir}/.stream.loop"
              printf "%s\n" ""
              printf '%s' "ID Purpose Client CircID CircPurpose Target"
              #printf '\n%6s %-20s %-21s %6s %-20s %-62s\n' "ID" "Purpose" "Client" "CircID" "CircPurpose" "Target"
              printf '%s' "------------------------------------------------------------------------------------------------------"
            }
            ## Stream purpose is unknown for tor-ctrl if it was an end of stream that we didn't catch the creation
            ## unknown us being specified for proper field separation
            printf "%s\n" "${stream_id} ${stream_purpose:="UNKNOWN"} ${stream_client:="UNKNOWN"} ${circuit_id}  ${circuit_purpose} ${stream_target}" | tr -s " "
            #printf '%6s %-20s %-21s %6s %-20s %-62s\n' "${stream_id}" "${stream_purpose}" "${stream_client}" "${circuit_id}" "${circuit_purpose}" "${stream_target}" | tr -s " "
          fi
          ## return to avoid duplicates (happens when there is CLOSED and SUCCEEDED)
          return
         ;;
      esac
    done
  done
}

#set -x

has tor-ctrl || error_msg "Install tor-ctrl"

while getopts ":s:p:a:mzh" Option; do
  case ${Option} in
    s)
      tor_control_socket="${OPTARG}"
      ;;
    p)
      tor_password="${OPTARG}"
      ;;
    m)
      machine_mode=1
      ;;
    z)
      no_circuit=1
      ;;
    a)
      client_filter="${OPTARG}"
      ;;
    h|*)
      usage
      ;;
  esac
done

cli_args=""
[ -n "${tor_control_socket}" ] && cli_args="${cli_args} -s ${tor_control_socket}"
[ -n "${tor_password}" ] && cli_args="${cli_args} -p ${tor_password}"
[ -n "${machine_mode}" ] && cli_args="${cli_args} -m"

temp_dir="$(mktemp --directory)"

if [ -z "${machine_mode}" ]; then
  printf '%s\n' "${me}: [info]: subscribed to Tor stream events, as soon as streams are created, output will be shown below."
  [ -n "${client_filter}" ] && printf '%s\n' "${me}: [info]: only streams created by the client address ${client_filter} will be shown"
  [ -z "${no_circuit}" ] && printf '%s\n' "${me}: [warn]: posting these contents online can deanonymize the tor client."
fi

## it will print the streams table after receiving an INT signal
## other signals such as QUIT, TERM and EXIT should kill the process tree and exit
trap "list_streams" INT
trap "exit" QUIT TERM
trap "exit_handler" EXIT

## this call will print to screen and sent to background
# shellcheck disable=SC2086
tor-ctrl -w -c "SETEVENTS STREAM" ${cli_args} | tee -- "${temp_dir}/.stream.tmp"
