#!/usr/bin/env sh

nocolor="\033[0m"
bold="\033[1m"

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

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
}


list_streams(){
  ##https://stackoverflow.com/a/22644006 and https://stackoverflow.com/a/53714583
  # shellcheck disable=SC2154
  trap "exit" INT QUIT TERM
  trap "rm -f "${tmpdir}"/.stream.hosts "${tmpdir}"/.stream.tmp "${tmpdir}"/.stream.loop; kill 0" EXIT

  listen_stream="$(cat "${tmpdir}"/.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" ); do
    printf '%s\n' "${listen_stream}" | grep "^650 STREAM ${stream_ordered}" | while IFS="$(printf '\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=//")"
          stream_client="$(printf '%s\n' "${stream_line}" | tr " " "\n" | grep "SOURCE_ADDR=" | sed "s/SOURCE_ADDR=//")"
	  test -n "${client_filter}" && [ "${client_filter%:*}" != "${stream_client%:*}" ] && break
        ;;
        REMAP)
          stream_target_cache="$(printf '%s\n' "$(cat "${tmpdir}"/.stream.hosts 2>/dev/null) ${stream_target_orig%:*}=${stream_target%:*}")"
          printf '%s\n' "${stream_target_cache}" | tee "${tmpdir}"/.stream.hosts >/dev/null
          stream_target_hostname="$(printf '%s\n' "${stream_target_cache}" | tr " " "\n" | grep -F "=${stream_target%:*}" | head -n 1 | sed "s|=.*||")"
          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")"
          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"${bold}------------------------------------------------------------------------------------------------------${nocolor}\n"
            printf %s"${bold}Target:${nocolor} ${stream_target}${nocolor}\n"
            printf %s"${bold}Stream:${nocolor} ${stream_id}, ${bold}Purpose:${nocolor} ${stream_purpose}, ${bold}Client:${nocolor} ${stream_client}${nocolor}\n"
            printf %s"${bold}Circuit:${nocolor} ${circuit_id}, ${bold}Purpose:${nocolor} ${circuit_purpose}${nocolor}\n"
            printf %s"${bold}"
            printf '%1s. %-40s %-15s %-19s %-4s %9s\n' "n" "Fingerprint" "Address" "Nickname" "Geo" "Bandwidth"
            printf %s"${bold}------------------------------------------------------------------------------------------------------${nocolor}\n"
            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)"
              relay_bandwidth="$(printf '%s\n' "${relay_status}" | grep "^w " | sed "s/w Bandwidth=//;s/\\r//")"
              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//")"
              printf '%1s. %-40s %-15s %-19s %-4s %9s\n' "${hop}" "${relay_fingerprint}" "${relay_host}" "${relay_nickname}" "(${relay_locale})" "${relay_bandwidth}"
            done
            printf %s"${bold}------------------------------------------------------------------------------------------------------${nocolor}\n\n"
          else
            ! test -f "${tmpdir}"/.stream.loop && {
              touch "${tmpdir}"/.stream.loop
              printf %s"${bold}"
              printf "\nID Purpose Client CircID CircPurpose Target\n"
              #printf '\n%6s %-20s %-21s %6s %-20s %-62s\n' "ID" "Purpose" "Client" "CircID" "CircPurpose" "Target"
              printf %s"------------------------------------------------------------------------------------------------------${nocolor}\n"
            }
            ## Stream purpose is unkown 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"${stream_id} ${stream_purpose:="UNKNOWN"} ${stream_client:="UNKNOWN"} ${circuit_id}  ${circuit_purpose} ${stream_target}\n" | 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

command -v tor-ctrl >/dev/null || 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"

: "${TMPDIR:="/tmp"}"
mkdir -p "${TMPDIR}/tor-ctrl-stream" || error_msg "Failed to create '${TMPDIR}/tor-ctrl-observer'"
tmpdir="$(mktemp -d ${TMPDIR}/tor-ctrl-stream/XXXXXX)"

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 "rm -f "${tmpdir}"/.stream.tmp; kill 0" EXIT

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