#!/bin/bash

## ADD_ONION - https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1748
## DEL_ONION - https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1925
## ONION_CLIENT_AUTH_ADD - https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1990
## ONION_CLIENT_AUTH_REMOVE - https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n2033
## ONION_CLIENT_AUTH_VIEW - https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n2051

## Once created the new Onion Service will remain active until either:
##  - the Onion Service is removed via "DEL_ONION",
##  - the server terminates,
##  - the control connection that originated the "ADD_ONION" command is closed.
## It is possible to override disabling the Onion Service on control connection close by specifying the "Detach" flag.

## 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

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

add_onion=""
client_pub_key=""
del_onion=""
discard_pk=""
flags=""
key=""
list_onion=""
machine_mode=""
max_streams=""
non_anonymous=""
onion=""
onion_client_auth_add=""
onion_client_auth_remove=""
onion_client_auth_view=""
onion_id_file_realpath=""
permanent_auth=""
permanent_onion_restore=""
permanent_onion_save=""
permanent_onion_save_realpath=""
port=""
tor_control_socket=""
tor_password=""
wait_confirmation=""

usage(){
  printf '%s\n' "usage: ${me} [-mh] [-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

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

  -h             print this help message

  -o             tor hostname, also known as HSAddress or ServiceId
                 notice: '.onion' is optional, it will be stripped

SERVER

 Add onion:

  -A             add onion
                 default: not set

  -k [key_type:key_blob]
                 use key specified by the argument
                 if 'key_type' is NEW, will create a new key using the
                   'key_blob' BEST algorithm, which currently is ED25519-V3
                 if 'key_type' is an algorithm, e.g. ED25519-V3, 'key_blob' is
                   the private key in base64
                 notice: useful to restore keys: 'ED25519-V3:onion_private_key
                 default: NEW:BEST

  -l [virtport,[target]]
                 notice: virtport: The virtual TCP Port for the Onion Service
                 notice: target: The (optional) target for the given VirtPort
                 default: not set

  -u [client_pub_key]
                 client public key in base64 encoding

  -w             don't detach onion service from controller
                 notice: the service will remain active until Ctrl+C
                 notice: the service won't be shown by 'GETINFO onions/current'
                 default: not set

  -i             discard service private key
                 notice: this is irreversible, it will not be possible to
                         recreate the same hostname

  -y             add a non-anonymous Single Onion Service.

  -x [max_streams]
                 when the number of max stream is reached, close circuit.

  -P [dir]       directory where to save permanent onions

  -E [dir|file]  directory or file search for onion(s) to restore

 Delete onion:

  -D             delete onion
                 notice: service is the hostname without '.onion'

 List onions:

  -L             a newline-separated list of the detached services created


CLIENT

 Add onion client authorization:

  -U [key_type:client_priv_key]
                 add client-side v3 client auth credentials for the onion
                 service
                 key_type: 'x2551' is the only one supported right now
                 client_priv_key: base64 encoding of x25519 key

  -n             permanent flag: this client's credentials should be stored in
                 the filesystem.
                 notice: If this is not set, the client's credentials are
                         ephemeral and stored in memory.

 Remove onion client authorization:

  -R             remove the client-side v3 client auth credentials for the
                 onion service

 View onion client authorizations:

  -W             view client-side credentials for specified onion service or
                 all credentials if none service was provided

Examples:
  Server
                 ${me} -A -l 80
                 ${me} -A -l 80,127.0.0.1:8080 -w -i
                 ${me} -A -l 80 -w -i -u client_pub_key
                 ${me} -A -P ~/.${me} -l 80
                 ${me} -A -E ~/.${me}
                 ${me} -D -o website.onion
                 ${me} -L
  Client
                 ${me} -U x25519:client_priv_key_base64 -o website.onion
                 ${me} -R -o website.onion
                 ${me} -W
                 ${me} -W -o website.onion
"
  exit 1
}

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

validate_onion(){
  onion_to_validate="$1"
  ## remove .onion suffix and everything after it
  onion_to_validate=${onion_to_validate%%.onion*}
  ## remove protocol (http://, git:// etc)
  onion_to_validate=${onion_to_validate#*://}
  ## count characters
  [ "${#onion_to_validate}" = "56" ] || error_msg "Onion '${onion_to_validate}' is invalid
  length '${#onion_to_validate}' should be exactly 56 characters long (not counting .onion)"
  ## check for base32 and lower case letters
  [ -n "${onion_to_validate%%*[^a-z2-7]*}" ] || error_msg "Onion '${onion_to_validate}' is invalid
  it is not within base32 alphabet lower-case encoding [a-z2-7]"
  printf "%s\n" "${onion_to_validate}"
}

exit_handler() {
  trap "" EXIT
  if ! test -d "${permanent_onion_save%*/}" ; then
    return 0
  fi
  safe-rm -f -- "${permanent_onion_save%*/}/onion.tmp"
  return 0
}

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

############
### opts ###

get_arg(){
  ## if argument is empty or starts with '-', fail as it possibly is an option
  case "${arg}" in
    ""|-*)
      error_msg "Option '${opt}' requires an argument."
      ;;
  esac
  value="${arg}"
  # shellcheck disable=SC2140
  check_variable_name "${1}" || exit 1
  printf -v value_quoted '%q' "${value}"
  eval "${1}"="${value_quoted}"
}

## hacky getopts
while :; do
  case "${1:-}" in
    -*)
      opt="${1#*-}"
      arg="${2:-}"
      ;;
    *)
      opt="${1:-}"
      ;;
  esac
  case "${opt}" in
    o)
      get_arg onion
      shift 2
      ;;

    ## server
    A)
      add_onion=1
      shift 1
      ;;
    k)
      get_arg key
      shift 2
      ;;
    l)
      get_arg port
      shift 2
      ;;
    w)
      wait_confirmation=1
      shift 1
      ;;
    u)
      get_arg client_pub_key
      shift 2
      ;;
    i)
      discard_pk=1
      shift 1
      ;;
    y)
      non_anonymous=1
      shift 1
      ;;
    x)
      get_arg max_streams
      shift 2
      ;;
    P)
      get_arg permanent_onion_save
      shift 2
      ;;
    E)
      get_arg permanent_onion_restore
      shift 2
      ;;

    L)
      list_onion=1
      shift 1
      ;;
    D)
      del_onion=1
      shift 1
      ;;

    ## client
    U)
      get_arg onion_client_auth_add
      shift 2
      ;;
    n)
      permanent_auth="Flags=Permanent"
      shift 1
      ;;
    R)
      onion_client_auth_remove=1
      shift 1
      ;;
    W)
      onion_client_auth_view=1
      shift 1
      ;;

    ## general
    s)
      get_arg tor_control_socket
      shift 2
      ;;
    p)
      get_arg tor_password
      shift 2
      ;;
    m)
      machine_mode=1
      shift 1
      ;;

    h)
      usage
      ;;
    -)
      shift 1
      break
      ;;
    "")
      break
      ;;
    *) ## it could be be usage instead of break, but then would need to use '--' to end option parsing
      break
      ;;
  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"
[ -n "${wait_confirmation}" ] && cli_args="${cli_args} -w"

#########
## Server

if [ -n "${list_onion}" ]; then
  # shellcheck disable=SC2086
  tor-ctrl ${cli_args} GETINFO onions/detached
  exit "${?}"
fi

if [ -n "${del_onion}" ]; then
  onion="$(validate_onion "${onion}")"
  # shellcheck disable=SC2086
  tor-ctrl ${cli_args} DEL_ONION "${onion}"
  exit "${?}"
fi

if [ -n "${add_onion}" ]; then
  : "${key:="NEW:BEST"}"
  [ -n "${port}" ] && port="Port=${port}"
  [ -z "${wait_confirmation}" ] && flags="${flags}Detach,"
  [ -n "${discard_pk}" ] && flags="${flags}DiscardPK,"
  [ -n "${client_pub_key}" ] && flags="${flags}V3Auth," client_pub_key="ClientAuthV3=${client_pub_key}"
  [ -n "${non_anonymous}" ] && flags="${flags}NonAnonymous,"
  [ -n "${max_streams}" ] && flags="${flags}MaxStreamsCloseCircuit," max_streams="MaxStreams=${max_streams}"
  [ -n "${flags}" ] && flags="Flags=${flags}"
  if [ -n "${permanent_onion_save}" ] && [ -n "${discard_pk}" ]; then
    error_msg "Permanent onion mode is not compatible with the flag that discards the private key."
  fi
  if [ -n "${permanent_onion_save}" ]; then
    test -d "${permanent_onion_save}" || error_msg "Permanent directory to save onion does not exist: ${permanent_onion_save}"
    trap "exit_handler" INT EXIT
    printf '%s\n' "${port} ${client_pub_key} ${max_streams} ${flags}" | tr -s " " | tee -- "${permanent_onion_save%*/}/onion.tmp"
    exit_code=0
    # shellcheck disable=SC2086,SC2248
    tor-ctrl ${cli_args} ADD_ONION "${key}" "${port}" ${client_pub_key} ${max_streams} ${flags} | grep -- "^250-" | tee -a -- "${permanent_onion_save%*/}/onion.tmp" \
      || exit_code="${?}"
    [ "${exit_code}" -ne 0 ] && error_msg "Failed to create onion"
    onion_id_file="$(grep -- "250-ServiceID=" "${permanent_onion_save}/onion.tmp" | sed "s/250-ServiceID=//" | tr -d "\\r")"
    onion_id_file="$(validate_onion "${onion_id_file}")"
    ## guard against directory traversal
    permanent_onion_save_realpath="$(realpath "${permanent_onion_save%*/}")"
    onion_id_file_realpath="$(realpath "${permanent_onion_save%*/}/${onion_id_file}")"
    if [ -z "${onion_id_file_realpath}" ]; then
      error_msg "Intended onion ID file path could not be resolved, possibly directory traversal attack attempt?"
    fi
    if [ -z "${permanent_onion_save_realpath}" ]; then
      error_msg "Permanent onion save directory path could not be resolved, directory deleted?"
    fi
    if ! [[ "${onion_id_file_realpath}" =~ ^"${permanent_onion_save_realpath}" ]]; then
      error_msg "Onion ID file would end up saved outside of permanent onion save directory, possible directory traversal attack attempt?"
    fi
    ## save file with its Service Identification, good practice to identify the service
    mv -- "${permanent_onion_save%*/}/onion.tmp" "${permanent_onion_save%*/}/${onion_id_file}"
    exit "${?}"
  elif [ -n "${permanent_onion_restore}" ]; then
    if test -f "${permanent_onion_restore}"; then
      restore_parameters="$(grep -E -- "Port=|Flags=|ClientAuthV3=|MaxStreams=" "${permanent_onion_restore}" | tr "\n" " ")"
      restore_key="$(grep -- "250-PrivateKey=" "${permanent_onion_restore}" | sed "s/250-PrivateKey=//")"
      # shellcheck disable=SC2086
      tor-ctrl ${cli_args} ADD_ONION ${restore_key} ${restore_parameters} | grep -- "^250-"
      exit "${?}"
    elif test -d "${permanent_onion_restore}"; then
      for file in "${permanent_onion_restore%*/}/"*; do
        restore_parameters="$(grep -E -- "Port=|Flags=|ClientAuthV3=|MaxStreams=" "${file}" | tr "\n" " ")"
        restore_key="$(grep -- "250-PrivateKey=" "${file}" | sed "s/250-PrivateKey=//")"
        # shellcheck disable=SC2086
        tor-ctrl ${cli_args} ADD_ONION ${restore_key} ${restore_parameters} | grep -- "^250-"
      done
      exit "${?}" ## yes, just the last try of exit code
    else
      error_msg "Permanent directory or file to restore onion does not exist: ${permanent_onion_restore}"
    fi
  else
    # shellcheck disable=SC2086,SC2248
    tor-ctrl ${cli_args} ADD_ONION "${key}" "${port}" ${client_pub_key} ${max_streams} ${flags} | grep -v -- "^250 "
  fi
  exit "${?}"
fi


#########
## Client

if [ -n "${onion_client_auth_add}" ]; then
  [ -z "${onion}" ] && error_msg "This option requires an onion service to be specified, use with '-o address.onion'"
  onion="$(validate_onion "${onion}")"
  # shellcheck disable=SC2086,SC2248
  tor-ctrl ${cli_args} ONION_CLIENT_AUTH_ADD "${onion}" "${onion_client_auth_add}" ${permanent_auth}
  exit "${?}"
fi

if [ -n "${onion_client_auth_remove}" ]; then
  [ -z "${onion}" ] && error_msg "This option requires an onion service to be specified, use with '-o address.onion'"
  onion="$(validate_onion "${onion}")"
  # shellcheck disable=SC2086
  tor-ctrl ${cli_args} ONION_CLIENT_AUTH_REMOVE "${onion}"
  exit "${?}"
fi

if [ -n "${onion_client_auth_view}" ]; then
  [ -n "${onion}" ] && onion="$(validate_onion "${onion}")"
  # shellcheck disable=SC2086
  tor-ctrl ${cli_args} ONION_CLIENT_AUTH_VIEW ${onion}
  exit "${?}"
fi

######
## if reached here and no major option was specified, show help message as nothing was run
usage
