Created
April 7, 2016 18:42
-
-
Save pyllyukko/3f80849877f39ae5be14b30576ecc1ed to your computer and use it in GitHub Desktop.
FTP client written in Bash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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