#!/bin/bash ################################################################################ # # # ViaThinkSoft - intelligent software for everyone # # # # File: /usr/bin/vtor # # Description: ViaThinkSoft TOR wrapper # # License: LGPL # # Author: Daniel Marschall # # Version: 1.4.4 # # Last change: 2015-01-12 # # # # Changelog: # # # # 2012-01-24 - 1.0 - daniel - Added version tag # # 2012-07-03 - 1.1 - daniel - Double spaces are now allowed (' ') # # 2012-07-13 - 1.2 - daniel - usewithtor is now hotfixed on the fly # # to allow commands which contain spaces. # # This also fixes a bogus error message # # Several other changes. # # 2012-11-25 - 1.3 - daniel - Parameters -e, -E, -n won't get filtered # # anymore (echo params) # # 2014-08-21 - 1.4 - daniel - Added -a|--filterstdout # # # ################################################################################ # vtor.sh - ViaThinkSoft TOR wrapper # (C)2011-2015 Daniel Marschall, ViaThinkSoft # VERSION='1.4.4 (2015-01-12)'; # # INSTALLATION # # Please save as /usr/bin/vtor and set the +x attribute with chmod. # # REQUIREMENTS # # This script requires # - bash # - which # - eval # - sed # - sleep # - pidof # - getopt # - tee # - (chmod) # - usewithtor or torify (which are requiring TOR) # # Problems with TOR programs, which are workarounded by the VTOR wrapper: # - Torify (based on tsocks -- TODO torsocks???) has following problems: # 1. Out of the box: It won't do any DNS resolving. (Is this still the case?) # - Usewithtor (based on torsocks) has following problems: # 1. Out of the box (Debian): res_send() error message for no reason. No way to disable it. # "libtorsocks: The symbol res_send() was not found in any shared library. The error reported was: not found!" # 2. The script is not capable of programs which contain whitespaces. # 3. There is a bogus error message when the user enters a command which is non-executable/non-existing: # "ERROR: is setuid. usewithtor will not work on a setuid executable." # - Both have the problems: # 1. Rrequests may fail with "SOCKS V4 connect rejected" resp. "SOCKS V5 connect failed" # 2. Some commands like "gwhois example.com" will show weird things (bogus error messages?) like # "00:07:25 libtorsocks(13685): Call to connect received on completed request 4", without even failing. # # --> This tool extends "usewithtor", supresses the annoying shared-library message and # retries the process when the connect-rejected message appears (but the output will still # contain this message). # # This tool also determinates wheter to use 'usewithtor' or 'torify' or a program chosen by the user. # If a request fails to often, the user can choose to go the unsafe way (without using TOR). # Also, this script detects if TOR is running or not. # # USAGE # # (see -h) # # LIMITATION # # Since "tee" (used for checking if connect-rejected message appeared) needs a small amount of runtime, # there *will* be race-conditions when stdout and stderr messages are directly sent after each other. # They will get sorted (stdout before stderr) instead of the correct order. # This does not happen when there is a bit time (the run-time of "tee") between stderr and stdout outputs. # # OPEN PROBLEMS / TODO # # - Is there any way to avoid the race condition to happen? # - Instead of wrapping usewithtor, extend with out functionality usewithtor! # - Publish with man page and package # - Check if shell command injections are possible (because of "eval"?) # PHP's escapeshellarg did successfully prevent $((3+5)) in evaluating. # - Muss man "-p" oder $CMD irgendwie escapen? (escapeshellcmd?) # - "vtor -- curl http://www.iso.ch" -> curl: (52) Empty reply from server # (happens also with torify and usewithtor) # - Under some conditions, VTOR will retry if the remote host (or DNS?) is down. # - Both, tsocks and torsocks, will leak ICMP data. # - Use tsocks or torsocks if torify or usewithtor is not available # # Workarounds searching # - Is there a possibility to use xargs and determine the exit status? # - Can getopt be used to not touch the quotes? -> Don't add, don't remove any quotes! # - Can my workaround 1+2 solution be somehow modularized so it can be easier be used by anyone? # ---------------------------------------------------------------------------------- # DEFAULT CONFIGURATION RETRY_LIMIT=10; RETRY_SLEEPTIME=1; MULTIPLY_SLEEPTIME=1; RETRY_ON_ERROR=0; UNSAFE_MODE=0; PROG=''; VERBOSE=0; FILTERSTDOUT=0; # ---------------------------------------------------------------------------------- # FUNCTIONS function require() { ( # subshell prevents that this function changes variables which are used temporary here X=$( which "$1" 2> /dev/null ); if [ -z "$X" ]; then echo "$0: Require '$1'. Cannot execute without it." >&2; exit 1; fi ) return $?; } function usage() { echo "ViaThinkSoft TOR wrapper"; echo "(C)Copyright 2011-2014 Daniel Marschall, ViaThinkSoft"; echo "Licensed under LGPL"; echo ""; echo "Executes with arguments using TOR connections."; echo "This wrapper contains some enhancements like retries to avoid temporary SOCKET"; echo "failures or the optional changing to unsecure procedures."; echo ""; echo "Usage: $0 [...] [--] [...]"; echo ""; echo "With as:"; echo " -h|--help"; echo " Shows this help page."; echo " -v|--verbose"; echo " Enable verbose mode. It will also forwarded to torify and usewithtor,"; echo " but not to user defined programs (defined by -p|--use_program)."; echo " -u|--allow-unsecured"; echo " Enable unsecure mode. It will bypass TOR when the retry-limit is reached"; echo " or if TOR is not running."; echo " -s|--sleeptime "; echo " Sleeping time in seconds between retries. Default: $RETRY_SLEEPTIME"; echo " -r|--max_retries "; echo " Maximal retries on SOCKS4/5 rejections. Default $RETRY_LIMIT"; echo " -c|--constant_sleeptime"; echo " If this flag is set, the sleep time between retries will stay constant."; echo " Otherwise, the sleeptime of each try is the configured sleeptime muliplied"; echo " with the current retry-number:"; echo " sleeptime(i) = cfg_sleeptime * i; i = 1..max_retries"; echo " -p|--use_program "; echo " Use specific program instead of the automatical selection."; if [ -n "$PROG" ]; then echo " Default: $PROG"; else echo " Default: usewithtor, or if not found, torify."; fi echo " If you want to use arguments you need to use nested quotes, e.g."; echo " vtor -p '\"torifier\" \"argument 1\" \"argument 2\"' -- lynx 'www.example.com'"; echo " Please note that '-v' is automatically forwarded to the torifier."; echo " -f|--retry_on_error"; echo " Also retry if the exitcode of is not 0."; echo " Usually, vtor will only retry if the TOR-proxy is temporary unavailable."; echo " -a|--filterstdout"; echo " Also filters the bogus tor warning messages from STDOUT and not only from STDERR."; echo " This is useful when you run a program which calls another program using '&>' to"; echo " capture its whole output (and outputs it to STDOUT afterwards, e.g. after processing)." echo ""; echo "ATTENTION! If you are using or , you **MUST** use"; echo "'--' to separate and ! Example:"; echo "$0 -- "; echo "Or, if supports '--', you can also use:"; echo "$0 -- -- "; echo ""; echo "Program version: $VERSION"; } function filtercond { # TODO: ATTENTION! due to this buffering, lynx does not work anymore!!! if [ $1 -eq 1 ]; then while IFS= read data; do if [ "$data" = "" ]; then echo "" else r="$( echo "$data" | grep -v "() was not found in any shared library. The error reported was: not found!" | grep -v "): Call to connect received on completed request"; # e.g. vtor gwhois example.com )" if [ "$r" != "" ]; then echo "$r" fi fi done else while IFS= read data; do echo "$data"; done fi } function unquote() { echo "$1" | sed "s/^'\(.*\)'$/\1/"; } function checkEvenNumberOfSingleQuotes() { # returns 0 if the number of single quotes of "$1" is even ( # subshell prevents that this function changes variables which are used temporary here # eliminate escaped '\'. all remaining '\' will escape something, e.g. a single quote R=$( echo "$1" | sed "s/\\\\\\\\//g" 2> /dev/null ); # '//' -> '' A=$( echo "$R" | sed "s/\\\'//g" 2> /dev/null ); # remove all \' (escaped) B=$( echo "$A" | sed "s/'//g" 2> /dev/null ); # remove all ' (unescaped) (( COUNT_UNESCAPED_SEMICOLON = ${#A} - ${#B} )); # count the difference (= number of unescaped) (( ODD = ($COUNT_UNESCAPED_SEMICOLON % 2 != 0) )); exit $ODD; ) return $?; } function maketemp() { TMPFILE=$( mktemp --suffix="_vtor" 2> /dev/null ); # If mktemp didn't work, we use an insecure name if [ -z "$TMPFILE" ]; then if [ -n "$1" ]; then TMPFILE="/tmp/tmp_vtor_$1_$$"; else TMPFILE="/tmp/tmp_vtor_$$"; fi fi touch "$TMPFILE"; echo "$TMPFILE"; } function cleanTempFiles() { if [ -f "$TMPFILE" ]; then rm "$TMPFILE"; fi if [ -f "$UWT_TMP" ]; then rm "$UWT_TMP"; fi } function checkTorRunning() { # Check if TOR is running # Find exact path of the TOR executable # Useful since "pidof tor" could find the PID of any script that is called "tor" # An user could otherwise run a fake "tor" executable to let VTOR operate insecure (=non-anonymous) system-wide!! TOREXECS=$( which -a 'tor' 2> /dev/null ); if [ -z "$TOREXECS" ]; then TOREXECS=$'/usr/sbin/tor\n/usr/bin/tor'; fi TORRUNNING=0; while read -r TOREXEC; do # Check if it is running TORPID=$( pidof "$TOREXEC" 2> /dev/null ); if [ ! -z "$TORPID" ]; then TORRUNNING=1; break; fi done <<< "$TOREXECS" return $TORRUNNING; } # ---------------------------------------------------------------------------------- # START OF THE PROGRAM # "which" needs to be checked at FIRST since require() uses which! (Otherwise there will be bogus messages) require "which"; require "bash"; require "chmod"; # require "eval"; require "sed"; require "sleep"; require "pidof"; require "getopt"; require "tee"; # require "usewithtor"; # require "torify"; # Note: The quotes are very important. Otherwise, 'a b|c' would be interpreted as 'a|b|c'. PARAMS=( "$@" ); # WORKAROUND 1 (Part 1) # Temporarily "escape" double-whitespaces 'a b' => 'a\ \ b', otherwise getopt will do 'a b' => 'a b' (bug?) TMP_PARAMS=(); for X in "${PARAMS[@]}"; do # Y=$( echo "$X" | sed 's/ /\\ /g' 2> /dev/null ); # With this trick we can also allow "-e", "-E", "-n" (= echo params) as parameters. echo won't filter them away... Y=$( echo "-- $X" | sed 's/^-- //g' | sed 's/ /\\ /g' 2> /dev/null ); TMP_PARAMS+=( "$Y" ); done PARAMS=( "${TMP_PARAMS[@]}" ); # Script from # http://www.unixboard.de/vb3/showthread.php?36246-getopts-ordentliche-Parameternamen&p=285798&viewfull=1#post285798 # Optimized: # - $RESTARGS works now (+ unquoted) # Still not working (TODO): # - Optional arguments (::) don't work with long options. # - For short options, no whitespace is allowed # Attention, unknown arguments, e.g. '-x' will be removed, not moved to $restargs optarr=( $( getopt --name "$0" --options 'hvucfas:r:p:' --long 'help,verbose,allow_unsecured,constant_sleeptime,retry_on_error,filterstdout,sleeptime:,max_retries:,use_program:' -- "${PARAMS[@]}" 2> /dev/null ) ); # WORKAROUND 1 (Part 2) # Undo the quoting for getopt '\ \ ' -> ' ' tmp_optarr=(); for X in "${optarr[@]}"; do # Y=$( echo "$X" | sed 's/\\$//' 2> /dev/null ); # see above Y=$( echo "-- $X" | sed 's/^-- //g' | sed 's/\\$//' 2> /dev/null ); tmp_optarr+=( "$Y" ); done optarr=( "${tmp_optarr[@]}" ); # WORKAROUND 2 # getopt has produced unique 'x x' 'y y' strings for arguments with white spaces, but they are splitted as |'x|x'|'y|y'| in the array. # let's split them like that: |'x x'|'y y'| # Note: The special case "a ' x" == 'a '\'' x' will be also accepted! # Is this the best solution? tmp_optarr=(); Y=""; INSIDE=0; for X in "${optarr[@]}"; do checkEvenNumberOfSingleQuotes "$X"; (( EVEN = ($? == 0) )); if (( INSIDE )); then if (( EVEN )); then # ... inside ... if [ -z "$Y" ]; then Y="$X"; else Y+=" $X"; fi else # ...end' tmp_optarr+=( "$Y $X" ); Y=""; INSIDE=0; fi else if (( EVEN )); then # 'begin_and_end' tmp_optarr+=( "$X" ); else # 'begin... if [ -z "$Y" ]; then Y="$X"; else Y+=" $X"; fi INSIDE=1; fi fi done if [ -n "$Y" ]; then # Has $Y still content? This should usually not happen tmp_optarr+=( "$Y" ); fi optarr=( "${tmp_optarr[@]}" ); # ---------------------------------------------------------------------------------- # ARGUMENT PROCESSING # Now process the arguments i=0; while true; do case "${optarr[$i]}" in -h|--help) usage; exit 0; ;; -v|--verbose) VERBOSE=1; ((i++)); ;; -u|--allow_unsecured) UNSAFE_MODE=1; ((i++)); ;; -s|--sleeptime) RETRY_SLEEPTIME=$( unquote "${optarr[$((i+1))]}" ); ((i=i+2)); ;; -r|--max_retries) RETRY_LIMIT=$( unquote "${optarr[$((i+1))]}" ); ((i=i+2)); ;; -c|--constant_sleeptime) MULTIPLY_SLEEPTIME=0; ((i++)); ;; -p|--use_program) PROG=$( unquote "${optarr[$((i+1))]}" ); ((i=i+2)); ;; -f|--retry_on_error) RETRY_ON_ERROR=1; ((i++)); ;; -a|--filterstdout) FILTERSTDOUT=1 ((i++)); ;; --) ((i++)); break; ;; *) # Should never happen echo "$0: Internal error while command-line-processing! Please report this error as bug." >&2; exit 2; ;; esac done # RESTARGS=${optarr[@]:i}; # i..end CMD="${optarr[i]}"; ARG="${optarr[@]:((i+1))}"; # (i+1)..end # Check for any argument list if [ -z "$CMD" ]; then usage >&2; exit 1; fi # ---------------------------------------------------------------------------------- checkTorRunning; TORRUNNING=$?; if (( TORRUNNING == 0 )); then if (( UNSAFE_MODE == 1 )); then echo "$0: Warning: TOR is not running. Use without TOR." >&2; # echo "$ARG" | xargs "$CMD"; # $CMD $ARG; eval "$CMD" "$ARG"; exit $?; else echo "$0: FATAL ERROR: TOR is not running!" >&2; echo "Use -u|--allow-unsecured to continue anyway." >&2; exit 1; fi; fi # ---------------------------------------------------------------------------------- PROGTYPE="user"; PROGARGS=""; # --- (1) USEWITHTOR --- # Find out program if it was not set by command line arguments if [ -z "$PROG" ]; then PROGTYPE="usewithtor"; PROG=$( which usewithtor 2> /dev/null ); fi UWT_TMP=""; if [[ -n "$PROG" && "$PROGTYPE" == "usewithtor" ]]; then if (( VERBOSE )); then if [ -z "$PROGARGS" ]; then PROGARGS="-v"; else PROGARGS+=" -v"; fi fi # Correct some bugs in "usewithtor" on the fly UWT_TMP=$( maketemp "vtor_uwt" 2> /dev/null ); cat "$PROG" | # Line 22 (usewithtor.in,v 1.3 2008-07-06 15:17:35) # Just a small missing whitespace in the license part, nothing important # @@ -22 +22 @@ # -#* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * # +# * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * sed 's/^#\* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. \*$/# \* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. \*/' | # Line 74 (usewithtor.in,v 1.3 2008-07-06 15:17:35) # This patch solves a whitespace-incompatibility # This fix also solves the error message that appears if the users chooses a non-executable/non-existing command: # "ERROR: is setuid. usewithtor will not work on a setuid executable." # @@ -74 +74 @@ # -if [ -u `which "$1"` ]; then # +if [ -u "`which "$1"`" ]; then sed 's/^if \[ -u `which "\$1"` \]; then$/if [ -u "`which "$1"`" ]; then/' | # Line 75 (usewithtor.in,v 1.3 2008-07-06 15:17:35) # This patch solves a whitespace-incompatibility # @@ -75 +75 @@ # - set_id $1 u # + set_id "$1" u sed 's/^\tset_id \$1 u$/\tset_id "$1" u/' | # Line 76 (usewithtor.in,v 1.3 2008-07-06 15:17:35) # This patch solves a whitespace-incompatibility # Without this fix, there would be also a bogus error message that appears if the users chooses a non-executable/non-existing command: # "ERROR: is setgid. usewithtor will not work on a setgid executable." # @@ -76 +76 @@ # -elif [ -g `which "$1"` ]; then # +elif [ -g "`which "$1"`" ]; then sed 's/^elif \[ -g `which "\$1"` \]; then$/elif [ -g "`which "$1"`" ]; then/' | # Line 77 (usewithtor.in,v 1.3 2008-07-06 15:17:35) # This patch solves a whitespace-incompatibility # @@ -77 +77 @@ # - set_id $1 g # + set_id "$1" g sed 's/^\tset_id \$1 g$/\tset_id "$1" g/' \ > "$UWT_TMP" 2> /dev/null; chmod +x "$UWT_TMP"; PROG="$UWT_TMP"; fi # --- (2) USEWITHTOR --- if [ -z "$PROG" ]; then PROGTYPE="torify"; PROG=$( which torify 2> /dev/null ); fi if [[ -n "$PROG" && "$PROGTYPE" == "torify" ]]; then if (( VERBOSE )); then # TODO QUE: seit wann unterstützt torify "-v"? # scheinbar gab es eine alte version ohne "-v" und mit tsocks anstelle torsocks if [ -z "$PROGARGS" ]; then PROGARGS="-v"; else PROGARGS+=" -v"; fi fi fi # --- (3) Error if nothing exists --- if [ -z "$PROG" ]; then echo "$0: ERROR - Can't find usewithtor or torify!" >&2; exit 1; fi # ---------------------------------------------------------------------------------- TMPFILE=$( maketemp "vtor" 2> /dev/null ); TOR_STATUS=-1; NOT_REJECTED=0; RETRY_COUNTER=0; until (( NOT_REJECTED )) || (( RETRY_COUNTER > RETRY_LIMIT+1 )); do if (( RETRY_COUNTER == RETRY_LIMIT+1 )); then if (( UNSAFE_MODE == 1 )); then echo "$0: Will try it WITHOUT TOR now!" >&2; # echo "$ARG" | xargs "$CMD"; # "$CMD" "$ARG"; eval "$CMD" "$ARG"; TORSTATUS=$?; fi break; fi if (( RETRY_COUNTER > 0 )); then if (( MULTIPLY_SLEEPTIME )); then (( SLEEPTIME=RETRY_SLEEPTIME*RETRY_COUNTER )); else (( SLEEPTIME=RETRY_SLEEPTIME )); fi echo "$0: Sleep $SLEEPTIME seconds and try another time ($RETRY_COUNTER/$RETRY_LIMIT)..." >&2; if (( SLEEPTIME > 0 )); then sleep $SLEEPTIME; fi fi ( # Important: first tee, then grep, otherwise the stderr/stdout outputs would be completely sorted ( # More info: http://stackoverflow.com/questions/6988288/linux-bash-how-to-unquote # We do not use xargs since it does not preserve the exit code # We do use eval since it interprets quoted arguments with whitespaces correctly (e.g. 'a b' 'c') # echo "$PROGARGS" "$CMD" "$ARG" | xargs "$PROG"; # "$PROG" "$PROGARGS" "$CMD" "$ARG"; eval "$PROG" "$PROGARGS" "$CMD" "$ARG"; exit $?; ) 3>&1 1>&2 2>&3 | # todo: ist es möglich, dass stream #3 normal verwendet wird von einem programm? tee "$TMPFILE" | filtercond 1; exit ${PIPESTATUS[0]}; # status of $PROG ) 3>&1 1>&2 2>&3 | # Change stdout<->stderr back again filtercond $FILTERSTDOUT; TOR_STATUS=${PIPESTATUS[0]}; cat "$TMPFILE" | grep -q "SOCKS V4 connect rejected:"; SOCKS4_GREP_STATUS=$?; cat "$TMPFILE" | grep -q "SOCKS V5 connect failed:"; SOCKS5_GREP_STATUS=$?; # Grep status: # without "-v" # 0 = existing (rejected) # 1 = not existing (not rejected) # 2 = grep error (we should interpret it as rejected to be safe) (( NOT_REJECTED = (SOCKS4_GREP_STATUS == 1) && (SOCKS5_GREP_STATUS == 1) )); if (( RETRY_ON_ERROR && NOT_REJECTED )); then (( NOT_REJECTED = (TOR_STATUS == 0) )); fi (( RETRY_COUNTER++ )); done cleanTempFiles; exit $TOR_STATUS;