#!/bin/sh
################################################################################
# ftp.sh 0.2 -- pyllyukko@maimed.org                                           #
#                                                                              #
# - needs a few hundred more error checks=)                                    #
# - works nicely as a poc though                                               #
# - uses only 2 external programs,                                             #
#   cat for downloading and bc for unit conversion (e.g. B->KiB),              #
#   which is unnecessary anyway=)                                              #
#                                                                              #
# TODO:                                                                        #
#   - unit_convert() -> passive_transfer() -> KiB/s etc...                     #
#                                                                              #
################################################################################
[ ${BASH_VERSINFO[0]} -lt 3 ] && {
  echo "error: bash version < 3!" 1>&2
  exit 1
}
declare -r  USER="anonymous"
declare -r  PASS="esko@jorma.com"
declare -i  CONNECTIONS=0
declare -i  PORT=21
declare -i  CODE
################################################################################
declare -r ERR="\033[0\;31m"
declare -r RST="\033[0m"
declare -r HL="\033[1m"
declare -r CR=$'\r'
################################################################################
declare -ra MESSAGE_PREFIX=('+' '++' "\\${ERR}!\\${RST}")
declare -ri SERVER=0
declare -ri CLIENT=1
declare -ri SERVER_ERROR=2
################################################################################
# FUNCTIONS                                                                    #
################################################################################
function interact() {
  [ ${CONNECTIONS} -lt 1 ] && {
    echo "${FUNCNAME}(): no connections" 1>&2
    return 1
  }
  printf "%s\r\n" "${1}" 1>&3
  shift 1
  get_reply ${*}
  return ${?}
}
################################################################################
function get_reply() {
  local EXPECTED_CODE
  [ ${#} -lt 1 ] && {
    echo "${FUNCNAME}(): error: no parameters" 1>&2
    return 1
  }
  while read 0<&3
  do
    [[ "${REPLY:0:3}" =~ "^[0-9][0-9][0-9]$" ]] && CODE="${REPLY:0:3}" || CODE=0
    case "${REPLY:0:4}" in
      [0-9][0-9][0-9]"-") echo "${MESSAGE_PREFIX[${SERVER}]}     ${REPLY#[0-9][0-9][0-9]-}" ;;
      ##########################################################################
      # END OF SERVER REPLY                                                    #
      ##########################################################################
      [0-9][0-9][0-9]" ")
	for EXPECTED_CODE in ${*}
	do
	  [ ${CODE} -eq ${EXPECTED_CODE} ] && {
	    echo "${MESSAGE_PREFIX[${SERVER}]} ${REPLY}"
	    return 0
	  }
	done
        ########################################################################
        # WRONG REPLY                                                          #
        ########################################################################
	echo -e "${MESSAGE_PREFIX[${SERVER_ERROR}]} ${REPLY}"
	return 1
      ;;
      *) echo "${FUNCNAME}(): warning: got unknown reply from server!" 1>&2 ;;
    esac
  done
  ##############################################################################
  # ACTUALLY WE SHOULDN'T GET THIS FAR                                         #
  ##############################################################################
  return 2
}
################################################################################
function open_socket () {
  [ ${CONNECTIONS} -gt 0 ] && {
    echo "${FUNCNAME}(): connection already established!"
    return 1
  }
  ##############################################################################
  # OPEN TCP CONNECTION AS FILE DESCRIPTOR 3 FOR READING AND WRITING           #
  ##############################################################################
  exec 3<>/dev/tcp/${1}/${PORT} && {
    ((CONNECTIONS++))
    echo "${MESSAGE_PREFIX[${CLIENT}]} connection established"
    return 0
  } || {
    #echo "${FUNCNAME}(): connection refused"
    return 1
  }
}
################################################################################
function close_socket() {
  [ ${CONNECTIONS} -lt 1 ] && {
    echo "${FUNCNAME}(): error: no connections" 1>&2
    return 1
  }
  echo "${MESSAGE_PREFIX[${CLIENT}]} closing connection at file descriptor 3"
  exec 3>&- && {
    ((CONNECTIONS--))
    return 0
  } || {
    echo "${FUNCNAME}(): error: failed to close socket" 1>&2
    return 1
  }
}
################################################################################
function connect() {
  #shift 1
  [ ${#} -ne 1 ] && {
    echo "${FUNCNAME}(): usage: open host"
    return 1
  }
  local HOST="${1}"
  open_socket "${HOST}" || return 1
  get_reply 220         || return 1
  login                 || return 1
  return 0
}
################################################################################
function initialize_passive_connection() {
  local IP
  local PORT
  interact "PASV" 227 || return 1
  [[ "${REPLY}" =~ "\(([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)\)" ]] || {
    echo "${FUNCNAME}(): unknown error at line $[${LINENO}-1]" 1>&2
    return 1
  }
  IP="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}.${BASH_REMATCH[4]}"
  PORT=$[(${BASH_REMATCH[5]}<<8)|${BASH_REMATCH[6]}]
  echo "${MESSAGE_PREFIX[${CLIENT}]} opening data connection to ${IP}:${PORT} as file descriptor 4"
  ##############################################################################
  # TODO: GET || PUT, THE CONNECTION DOESN'T HAVE TO BE "BI-DIRECTIONAL" (<>)  #
  ##############################################################################
  exec 4<>/dev/tcp/${IP}/${PORT} && {
    return 0
  } || {
    echo "${FUNCNAME}(): error at line $[${LINENO}-3]!" 1>&2
    return 1
  }
}
################################################################################
function disconnect() {
  interact "QUIT" 221 || return 1
  close_socket        || return 1
  return 0
}
################################################################################
function directory_listing() {
  local LIST_CMD
  [ -z "${1}" ] && LIST_CMD="LIST" || LIST_CMD="LIST ${1}"
  initialize_passive_connection
  interact "${LIST_CMD}" 150 || {
    echo "${FUNCNAME}(): error at line $[${LINENO}-1]!" 1>&2
    return 1
  }
  echo "${MESSAGE_PREFIX[${CLIENT}]} receiving data"
  ##############################################################################
  # READ THE DIRECTORY LISTING                                                 #
  ##############################################################################
  while read 0<&4
  do
    echo -e "${HL}${REPLY}${RST}"
  done
  echo "${MESSAGE_PREFIX[${CLIENT}]} closing data connection at file descriptor 4"
  exec 4>&- || {
    echo "${FUNCNAME}(): error: failed to close data connection (fd 4) at line $[${LINENO}-1]!" 1>&2
    return 1  
  }
  get_reply 226 || {
    echo "${FUNCNAME}(): error at line $[${LINENO}-1]!" 1>&2
    return 1
  }
  return 0
}
################################################################################
function passive_transfer() {
  local -i  SIZE
  local -i  PID
  local -ai STOPWATCH
  local     FILENAME="${2##*/}"
  #interact "MLST ${1}" 250
  ##############################################################################
  # TODO: CLEAN THIS MESS                                                      #
  ##############################################################################
  [ "x${1}" = "xget" ] && {
    interact "SIZE ${2}" 213 && : || return 1
    [[ "${REPLY}" =~ "^.+ ([0-9]+)${CR}$" ]] && SIZE="${BASH_REMATCH[1]}" || :
  } || {
    :
  }
  #echo "DEBUG: ->${SIZE}<-"
  interact "TYPE I" 200         || return 1
  initialize_passive_connection || return 1
  ##############################################################################
  # HERE'S OUR DOWNLOAD -- CAT=)                                               #
  ##############################################################################
  case "${1}" in
    "get")
      (
        echo "${MESSAGE_PREFIX[${CLIENT}]} [child]: child process spawned (pid ${!})"
        cat - 0<&4 1>"${FILENAME}" 2>/dev/null
	echo "${MESSAGE_PREFIX[${CLIENT}]} [child]: child process finished"
      ) &
      PID=${!}
      interact "RETR ${2}" 150 || return 1
    ;;
    "put")
      interact "STOR ${2}" 150 || return 1
      ##########################################################################
      # command group () or block of code {}, does it matter?                  #
      ##########################################################################
      (
        echo "${MESSAGE_PREFIX[${CLIENT}]} [child]: child process spawned (pid ${!})"
        cat "${2}" 1>&4 2>/dev/null
	echo "${MESSAGE_PREFIX[${CLIENT}]} [child]: child process finished"
      ) &
      PID=${!}
    ;;
  esac
  STOPWATCH[0]="${SECONDS}"
  ##############################################################################
  # INSERT FANCY TRANSFER'O'METER HERE!                                        #
  ##############################################################################
  echo "${MESSAGE_PREFIX[${CLIENT}]} [parent]: waiting for child process to finish"
  wait "${PID}"
  echo "${MESSAGE_PREFIX[${CLIENT}]} closing data connection at file descriptor 4"
  exec 4>&-
  STOPWATCH[1]="${SECONDS}"
  STOPWATCH[2]=$[${STOPWATCH[1]}-${STOPWATCH[0]}]
  get_reply 226
  echo "${MESSAGE_PREFIX[${CLIENT}]} file transferred in ${STOPWATCH[2]} seconds"
  return 0
}
################################################################################
function modtime() {
  interact "MDTM ${1}" 213 || return 1
  [[ "${REPLY}" =~ "([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])([0-9][0-9])" ]] && {
    local YEAR="${BASH_REMATCH[1]}"
    local MONTH="${BASH_REMATCH[2]}"
    local DAY="${BASH_REMATCH[3]}"
    local HOURS="${BASH_REMATCH[4]}"
    local MINUTES="${BASH_REMATCH[5]}"
    local SECONDS="${BASH_REMATCH[6]}"
    echo "${MESSAGE_PREFIX[${CLIENT}]} ${1} -- ${DAY}.${MONTH}.${YEAR} ${HOURS}:${MINUTES}:${SECONDS}"
    return 0
  } || {
    return 1
  }
}
################################################################################
function size() {
  local -i BYTES
  local    SIZE
  local    UNIT
  interact "SIZE ${1}" 213 || return 1
  [[ "${REPLY}" =~ "^.+ ([0-9]+)${CR}$" ]] && {
    BYTES="${BASH_REMATCH[1]}"
  } || {
    echo "${FUNCNAME}(): error at line $[${LINENO}-3]" 1>&2
    return 1
  }
  if [ ${BYTES} -lt 1024 ]
  then
    SIZE="${BYTES}"
    UNIT="bytes"
  elif [ ${BYTES} -ge 1024 -a ${BYTES} -lt $[1024*1024] ]
  then
    SIZE=`echo "scale=2;${BYTES}/1024" | bc`
    UNIT="KiB"
  elif [ ${BYTES} -ge $[1024*1024] -a ${BYTES} -lt $[1024*1024*1024] ]
  then
    SIZE=`echo "scale=2;${BYTES}/1024/1024" | bc`
    UNIT="MiB"
  elif [ ${BYTES} -ge $[1024*1024*1024] ]
  then
    SIZE=`echo "scale=2;${BYTES}/1024/1024/1024" | bc`
    UNIT="GiB"  
  else
    echo "${FUNCNAME}(): error: cannot compute!-)" 1>&2
    return 1
  fi
  echo "${MESSAGE_PREFIX[${CLIENT}]} ${1} -- ${SIZE} ${UNIT}"
  return 0
}
################################################################################
function login() {
  printf "USER %s\r\n" "${USER}" 1>&3
  get_reply 230 331
  case "${CODE}" in
    ############################################################################
    # 230 == USER NAME IS ENOUGH FOR LOGIN                                     #
    ############################################################################
    "230") return 0                                ;;
    "331") interact "PASS ${PASS}" 230 || return 1 ;;
    *)     return 1                                ;;
  esac
  return 0
}
################################################################################
function ftp_help() {
  cat <<- EOF
	available commands:
	  cd delete dir disconnect get help lcd modtime open passive pwd quit size status system
EOF
  return ${?}
}
################################################################################
# "main()"                                                                     #
################################################################################
echo "${0##*/} -- pyllyukko@maimed.org"
while read -p '> ' -a REPLY
do
  case "${REPLY[0]}" in
    "cd")          interact          "CWD ${REPLY[1]}" 250  ;;
    "delete")      interact          "DELE ${REPLY[1]}" 250 ;;
    "dir")         directory_listing "${REPLY[1]}"          ;;
    "disconnect")  disconnect                               ;;
    "get")         passive_transfer  get "${REPLY[1]}"      ;;
    "help")        ftp_help                                 ;;
    "lcd")         pushd             "${REPLY[1]}"          ;;
    "modtime")     modtime           "${REPLY[1]}"          ;;
    "open")        connect           "${REPLY[1]}"          ;;
    "put")         passive_transfer  put "${REPLY[1]}"      ;;
    "pwd")         interact          "PWD"  257             ;;
    "size")        size              "${REPLY[1]}"          ;;
    "status")      interact          "STAT" 211             ;;
    "system")      interact          "SYST" 215             ;;
    "passive")
      echo "${MESSAGE_PREFIX[${CLIENT}]} passive is the _only_ supported mode=)"
    ;;
    "quit"|"exit")
      [ ${CONNECTIONS} -gt 0 ] && disconnect
      break
    ;;
    *) echo "unrecognized command \`${REPLY[0]}'" 1>&2     ;;
  esac
done
echo "bye now!"
exit 0
