Skip to content

Instantly share code, notes, and snippets.

@pyllyukko
Created April 7, 2016 18:42
Show Gist options
  • Save pyllyukko/3f80849877f39ae5be14b30576ecc1ed to your computer and use it in GitHub Desktop.
Save pyllyukko/3f80849877f39ae5be14b30576ecc1ed to your computer and use it in GitHub Desktop.
FTP client written in Bash
#!/bin/sh
################################################################################
# ftp.sh 0.2.1 -- pyllyukko #
# #
# - 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... #
# - command line parameters #
# - http://www.ietf.org/rfc/rfc959.txt #
# #
################################################################################
[ ${BASH_VERSINFO[0]} -lt 3 ] && {
echo "error: bash version < 3!" 1>&2
exit 1
}
[ ${BASH_VERSINFO[0]} -eq 4 ] && shopt -s compat31
declare -r USER="anonymous"
declare -r PASS="[email protected]"
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() {
# interact() -- send query and get response #
[ ${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() {
# open_socket() -- opens the control connection #
[ ${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
}
} # open_socket()
################################################################################
function close_socket() {
# close_socket() -- closes the control connection #
[ ${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
}
} # close_socket()
################################################################################
function connect() {
#shift 1
[ ${#} -ne 1 ] && {
echo "${FUNCNAME}(): usage: open host"
return 1
}
local HOST="${1}"
open_socket "${HOST}" || return 1
# 220 Service ready for new user.
get_reply 220 || return 1
login || return 1
return 0
}
################################################################################
function initialize_passive_connection() {
# initialize_passive_connection() -- opens a data connection #
local IP
local PORT
# 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
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 FILE DESCRIPTOR 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() {
# 221 Service closing control connection.
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
# 150 File status okay; about to open data 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
}
# 226 Closing data connection.
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" ] && {
# 213 File status.
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 ${BASHPID})"
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 ${BASHPID})"
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 (pid ${PID})"
# wait for cat 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]}]
# 226 Closing data connection.
get_reply 226
echo "${MESSAGE_PREFIX[${CLIENT}]} file transferred in ${STOPWATCH[2]} seconds"
[ -n "${SIZE}" -a ${STOPWATCH[2]} -ne 0 ] && echo "${MESSAGE_PREFIX[${CLIENT}]} ~$[${SIZE}/${STOPWATCH[2]}] bytes/second"
return 0
}
################################################################################
function modtime() {
# 213 File status.
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
} # size()
################################################################################
function login() {
printf "USER %s\r\n" "${USER}" 1>&3
get_reply 230 331
# rfc959:
# USER
# 230
# 530
# 500, 501, 421
# 331, 332
case "${CODE}" in
############################################################################
# 230 User logged in, proceed. #
############################################################################
"230") return 0 ;;
############################################################################
# 331 User name okay, need password. #
############################################################################
"331") interact "PASS ${PASS}" 230 || return 1 ;;
*) return 1 ;;
esac
return 0
} # login()
################################################################################
function ftp_help() {
cat 0<<- EOF
available commands:
cd delete dir disconnect get help lcd modtime open passive pwd quit size status system
EOF
return ${?}
} # ftp_help()
################################################################################
# "main()" #
################################################################################
echo "${0##*/} -- [email protected]"
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment