#!/usr/bin/env bash 

  ######################################################################
 ######################################################################
##              (2024) Victrixsoft - Copyrighted 
##
## Author: Adam M. Danischewski, Created: 20240616, Version: v1.13
## Last Modified: 20241010
##
## Issues: If you find any issues please open a ticket on github! 
##         https://github.com/victrixsoft/bashbro/issues
##
## Bashbro is a shell script that allows you to browse the filesystem,  
## view files and download them via web browser. Bashbro can be deployed 
## locally or on a remote server provided you have access to it and the port 
## you run it on is not firewalled or occupied.  
##
## bashbro -s -p=5669 will run bashbro on port 5669, you can then use  
## your browser to view files on localhost:5669 or via ip address/dns name   
## from any other computer that has access to the local computer server/port.  
## 
## Rework of bashttpd: https://github.com/avleen/bashttpd
## 
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
 ######################################################################
  ######################################################################

function exit_gracefully() {  
  exit 0 
}
trap "{ exit_gracefully; }" EXIT SIGINT SIGHUP SIGQUIT SIGTERM 

[[ -r "/dev/random" ]] && DISCARD_DEV="/dev/random" || DISCARD_DEV="/dev/null"
declare -r DISCARD_DEV
declare -i LISTEN_PORT=8880
declare -r FAVICON="iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAdJJREFUWEdjZMACumZN6/7PwFCCTY5cMUYGhp6ytKxSdP2MyAJds2YZ/Gf4c55cS4jRx8jAYliWlnYBphbugK45M/7///ePGDMoVsPIxMRQlpIBthtM0MPnmEEPCQmwAzpnTftPsbfIMKA8LYuRkRYJjli3gBIm40D5Hp4IB5UDylIz8Ybe/LUrGV6/e4eh5s+fPwxVmbk49b56+4ZhwbrVWOVRooCQA2AmdM2ejmIYIQfg0gfOhshRAHNAx8ypDExMTBguhslvP7Sf4fLNG3B5mAPevH/HMG/NSgx93vZODNpq6mBxdMeT5ACQATBHIBtEyAG49JEcAqMOwBb8oFAhFAUwfe0zpjAwMzOjpBGScsHLN68ZFq5fQ3I2xJb4sBZEgz4b8vHwMGRExmJkJ0JRMChyARsrK0NBQgrDzfv3GDbu2QmPRpLLgX///zNUpGUxLN+ykeHx82dgg4gJAVyhQLID1BSVGAJc3FFKNLo6gNKSsHP2dEgzDApIyoYwTaA4BMUlDBBbGRFdF2BkdCQB9MoEOQ3g04etEMKoC/AZQCu5gW+SDXijFBS0A9UuBDfLwSmTDl0y9DQE66INjq4ZzHX0CAmcnVPkIKJFwsTVPQcALWF/GjSWyzQAAAAASUVORK5CYII="               
declare    PROG="${0}"
declare    OLDOPTS=""
declare -i DEBUG=1 
declare -r DEBUG_OUTPUT="/tmp/__${0##*/}__"
declare -i VERBOSE=1
declare -r VERSION=1.13
declare -a REQUEST_HEADERS
declare    REQUEST_URI="" 
declare    JAIL=".." 
declare    JAIL_FILE="" 
declare -r FAIL_FLAG=1
declare -r SUCCESS_FLAG=0
declare -i START_SERVER=0
declare -A CLI_C=(
   ["YELLOW"]="\033[0;1;33m"
   ["RED"]="\033[0;0;31m"
   ["LRED"]="\033[0;1;31m"
   ["CYAN"]="\033[0;0;36m"
   ["LCYAN"]="\033[0;1;36m"
   ["PURPLE"]="\033[0;0;35m"
   ["LPURPLE"]="\033[0;1;35m"
   ["GREEN"]="\033[0;0;32m"
   ["LGREEN"]="\033[0;1;32m"
   ["ORANGE"]="\033[0;0;33m"
   ["LORANGE"]="\033[0;1;33m"
   ["GRAY"]="\033[0;0;37m"
   ["LGRAY"]="\033[0;1;37m"
   ["NC"]="\033[0m")
declare -a HTTP_RESPONSE=(
   [200]="OK"
   [400]="Bad Request"
   [403]="Forbidden"
   [404]="Not Found"
   [405]="Method Not Allowed"
   [500]="Internal Server Error")
declare DATE=$(date +"%a, %d %b %Y %H:%M:%S %Z")
declare -a RESPONSE_HEADERS=(
      "Date: $DATE"
   "Expires: $DATE"
    "Server: Bashbro - Bash HTTP File Server"
)

function warn() { ((VERBOSE)) && echo "WARNING: $*" >&2; }
function send() { ((VERBOSE)) && echo "> $*" >&2; echo "$*"; ((DEBUG)) && echo "< $*" >> "${DEBUG_OUTPUT}"; }

function add_response_header() { RESPONSE_HEADERS+=("$1: $2"); }

function send_response_binary() {
 local file="${2}" 
 local transfer_stats="" 
 local tmp_stat_file="/tmp/_send_response_$$_"
 send "HTTP/1.0 $1 ${HTTP_RESPONSE[$1]}"
 for i in "${RESPONSE_HEADERS[@]}"; do
    send "$i"
 done
 send
 if ((VERBOSE)); then 
  dd 2>"${tmp_stat_file}" < "${file}" 
  transfer_stats=$(<"${tmp_stat_file}") 
  echo -en ">> Transferred: ${file}\n>> $(sed -n '/copied/p' <<< "${transfer_stats}")\n" >&2  
  rm "${tmp_stat_file}"
 else 
  dd 2>"${DISCARD_DEV}" < "${file}"   
 fi 
}   

function send_response() {
  send "HTTP/1.0 $1 ${HTTP_RESPONSE[$1]}"
  for i in "${RESPONSE_HEADERS[@]}"; do
     send "$i"
  done
  send
  while IFS= read -r line; do
     send "${line}"
  done
}

function send_response_ok_exit() { send_response 200; exit 0; }

function send_response_ok_exit_binary() { send_response_binary 200  "${1}"; exit 0; }

function fail_with() { send_response "$1" <<< "$1 ${HTTP_RESPONSE[$1]}"; exit 1; }

function serve_file() {
  local file="${JAIL_FILE:-$1}"
  local CONTENT_TYPE=""
  case "${file}" in
    *\.css)
      CONTENT_TYPE="text/css"
      ;;
    *\.js)
      CONTENT_TYPE="text/javascript"
      ;;
    *)
      CONTENT_TYPE=$(file -b --mime-type "${file}")
      ;;
  esac
  add_response_header "Content-Type"  "${CONTENT_TYPE}"
  CONTENT_LENGTH=$(stat -c'%s' "${file}") 
  add_response_header "Content-Length" "${CONTENT_LENGTH}"
  send_response_ok_exit_binary "${file}"
}

function serve_dir_with_tree() {
  local dir="$1" 
  local no_favicon=" <link href=\"data:image/x-icon;base64,${FAVICON}\" rel=\"icon\" type=\"image/x-icon\" />"  
  local tree_page=""
  local base_server_path="/${2%/}"
  [ "$base_server_path" = "/" ] && base_server_path=".." 
  [[ "$JAIL" != ".." && ! "$dir" =~  ^"$JAIL" ]] && dir="$JAIL" && base_server_path="$JAIL"
  local tree_opts="--du -C -h -a --dirsfirst" 
  add_response_header "Content-Type" "text/html"
  tree_page=$(tree -H "${base_server_path}" -L 1 ${tree_opts} -D "${dir}")
  tree_page=$(sed "5 i ${no_favicon}" <<< "${tree_page}")  
  tree_page=$(sed '6 i <meta http-equiv="Content-Language" content="en">' <<< "${tree_page}")
  tree_page=$(sed '/href/s/#/%23/g' <<< "${tree_page}")
  send_response_ok_exit <<< "${tree_page}"  
}

function serve_dir() { serve_dir_with_tree "$@"; fail_with 500; }

function urldecode() { [ "${1%/}" = "" ] && echo "/" ||  echo -e "$(sed 's/%\([[:xdigit:]]\{2\}\)/\\\x\1/g' <<< "${1%/}")"; } 

function serve_dir_or_file_from() {
  local URL_PATH="${1}/${3}"
  shift
  URL_PATH="$(urldecode "${URL_PATH}")"
  if [[ -n "${JAIL_FILE}" && -f "${JAIL_FILE}" && -r "${JAIL_FILE}" ]]; then 
    serve_file "${JAIL_FILE}" "$@" || fail_with 403
  elif [[ -f "${URL_PATH}" && -r "${URL_PATH}" ]]; then
    [[ "$JAIL" != ".." ]] && [[ ! "$URL_PATH" =~ ^$JAIL ]] && fail_with 403
    serve_file "${URL_PATH}" "$@" || fail_with 403
  elif [[ -d "${URL_PATH}" ]]; then
   [[ "$URL_PATH" == *..* ]] && fail_with 400
   [[ -x "${URL_PATH}" ]] && \
    serve_dir  "${URL_PATH}" "$@" || fail_with 403
  fi
  fail_with 404
}

function on_uri_match() {
 local regex="$1"
 shift
 [[ "${REQUEST_URI}" =~ $regex ]] && "$@" "${BASH_REMATCH[@]}"
}
 
function run() { 
 local line="" 
 local REQUEST_METHOD=""
 local REQUEST_HTTP_VERSION="" 
 read -r line || fail_with 400
 line=${line%%$'\r'}
 read -r REQUEST_METHOD REQUEST_URI REQUEST_HTTP_VERSION <<< "${line}"
 [ -n "${REQUEST_METHOD}" ] && [ -n "${REQUEST_URI}" ] && \
 [ -n "${REQUEST_HTTP_VERSION}" ] || fail_with 400
 [ "${REQUEST_METHOD}" = "GET" ] || fail_with 405
 while IFS= read -r line; do
  line=${line%%$'\r'}
  [ -z "${line}" ] && break
  REQUEST_HEADERS+=("${line}")
 done
} 

function check_reqs() {
 local -i PASS_OR_FAIL=0
 local tver=""
 getopt -T
 [ "$?" != 4 ] && { echo "Wrong version of 'getopt' .." && PASS_OR_FAIL+=1; }
 which sed &>"${DISCARD_DEV}" || { echo "Missing sed .. " && PASS_OR_FAIL+=1; }
 which socat &>"${DISCARD_DEV}" || { echo "Missing socat .. " && PASS_OR_FAIL+=1; }
 if which tree &>"${DISCARD_DEV}"; then 
  read _ tver x < <(tree --version)
  tver=${tver//.};tver=${tver//v};tver=${tver:0:2}
  ((tver<16)) && echo "Incorrect tree version, ${0##*/} requires v1.6+ .. " && PASS_OR_FAIL+=1  
 else 
  echo "Missing tree .. "
  PASS_OR_FAIL+=1
 fi
 ((PASS_OR_FAIL>0)) && { echo "Failed dependencies check, exiting .. " && exit 1; }
}

function usage() {
 local RETURN_CODE=${SUCCESS_FLAG}
read -r -d '' USAGE <<EOF || RETURN_CODE=${FAIL_FLAG}
${CLI_C["LCYAN"]}Usage:${CLI_C["NC"]} ${CLI_C["YELLOW"]}${0##*/}\
${CLI_C["NC"]} <-s [-p=<port #>] [-j|--jail]> | [<-v|--version>|<-h|--help>] 
 
  ${CLI_C["LCYAN"]}Description:${CLI_C["NC"]} Bashbro starts a bash\
web-based file server, after 
               launching connect to the server via web browser. 
               Default port is 8880, jailed files only serve that file

    ${CLI_C["LCYAN"]}  Options:${CLI_C["NC"]} 
             -?/-h/--help    --     Show usage
               -s/--start    --     Start Bashbro - Http file server
                -p/--port <port #>  Set port for socat to listen on  
                -j/--jail  <path>   Sets jail valid path to dir/file  
             -v/--version    --     Print version
             -V/--verbose    --     Verbose
      
 ${CLI_C["LGREEN"]}  E.g. %> ${0##*/} -sp5544 -j/tmp\
  # Now open localhost:5544${CLI_C["NC"]}  
 
 ${CLI_C["ORANGE"]} ** Note: on Chrome browsers, you may need to use your IP address
     instead of localhost (e.g. 192.1.168.x:8880) ${CLI_C["NC"]}
EOF
 echo -e "$USAGE"
 return ${RETURN_CODE}
} 

function error_nonexist_opt() {
 echo -e " ${CLI_C["RED"]}>>> Error, nonexistent option: \"$1\"${CLI_C["NC"]}" >&2
 exit 1
} 

function error_missing_port() { 
 echo -e "${CLI_C["RED"]} >>> Error, expected a port number for\
 -p/--port option (omit -p for default port: ${LISTEN_PORT}).${CLI_C["NC"]}"
 exit 1
}

function error_bad_jail() { 
 echo -e "${CLI_C["RED"]} >>> Error, jail location \"${1}\"\
 not found or inaccessible (check perms).${CLI_C["NC"]}"
 exit 1
}

function proc_opts() {
 local -r OLDOPTS="$(shopt -po errexit)"
 set -e
 [[ "$1" =~ -[?] ]] && { usage; exit 0; }
 local params="$(getopt -o p:vj:shV -l port:,version,start,jail,help,verbose --name "${0##*/}" -- "$@")"
 eval set -- "$params" && unset params
 while true
 do
    case "$1" in
        -s|--start)
            START_SERVER=1
            ;;
        -j|--jail)
            if [[ -n "${2}" ]]; then 
              if [[ ! "${2}" =~ ^-  &&  -n "${2//[[:space:]]}" ]]; then 
                if [[ -n "${2#=}" && -r "${2#=}" ]]; then 
		  JAIL=${2#=}
		  [[ -f "${JAIL}" ]] && JAIL_FILE="${JAIL}" 
	        else
		  error_bad_jail "${2#=}"
		fi
              else             
		error_bad_jail "${2#=}"
              fi 
              shift
            else 
	     error_bad_jail "${2#=}"
            fi
            ;;
        -p|--port)
            if [[ -n "${2}" ]]; then 
              if [[ ! "${2}" =~ ^- ]] && [[ -n "${2//[[:space:]]}" ]]; then 
               { [[ -n "${2#=}" ]] && [[ "${2#=}" =~ ^[0-9]+$ ]] && \
                LISTEN_PORT=${2#=}; } || error_missing_port
              else             
                error_missing_port
              fi 
              shift
            else 
             error_missing_port
            fi
            ;;
        -v|--version)
            echo -e "${CLI_C["YELLOW"]}Bashbro - v${VERSION} ${CLI_C["NC"]}"
            exit 0
            ;;
        -V|--verbose)
            VERBOSE=2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        --)
            break
            ;;
        *) 
            [[ -z ${1//[[:space:]]} ]] && break ## All good, was an optional arg            
            error_nonexist_opt ${1}
            ;;
    esac
    shift  
 done
 eval "$OLDOPTS"
 [[ -n $2 ]] && error_nonexist_opt ${2}
}

function main() { 
 check_reqs
 proc_opts "$@"
 if ((START_SERVER==1)); then
  [[ ${UID} = 0 ]] && warn "It is not recommended to run ${0##*/} as root."
  echo -e "${CLI_C["GREEN"]}Starting Bashbro on port: $LISTEN_PORT .. ${CLI_C["NC"]}"
  if [[ "${JAIL}" != ".." ]]; then 
   [[ -n "$JAIL_FILE" ]] && echo -e "${CLI_C["ORANGE"]}Jailed to serving file: $JAIL_FILE ..${CLI_C["NC"]}" \
   || echo -e "${CLI_C["ORANGE"]}Jailed to serving directory: $JAIL ..${CLI_C["NC"]}"  
  fi  
  socat TCP4-LISTEN:${LISTEN_PORT},fork EXEC:"${PROG} -j=${JAIL}" 2>"${DISCARD_DEV}" 
 else 
  run 
  on_uri_match '/(.*)' serve_dir_or_file_from 
  fail_with 500
 fi  
}

main "$@"
