#!/usr/bin/env python3 import argparse import socket import sys import json import ssl import time import os # store these as global variables # for easy access USER = None PSWD = None PORT = None PATH = None HOST = None # list of operations that can be executed on command line AVAILABLE_OPS = {'ls', 'mkdir', 'rm', 'rmdir', 'cp', 'mv'} TWOARG_OPS = {'cp', 'mv'} ONEARG_OPS = {'ls', 'mkdir', 'rm', 'rmdir'} # parse the given url to # retrieve connection info def parse_url_info(url): global USER global PSWD global PORT global PATH global HOST # anonymous user case if url.index('@') == -1: USER = 'anonymous' HOST = url[url.index('6'):url.index(':')] # get user and password else: url_parts = url.split('@') user_pass = url_parts[0][6:].split(':') USER = user_pass[0] PSWD = user_pass[1] host_port_path = url_parts[1].split('/', 1) PATH = host_port_path[1] host_port = host_port_path[0].split(':') HOST = host_port[0] if len(host_port) > 1: PORT = int(host_port[1]) else: PORT = 21 # default port is 21 # validate the operation def check_operation(op, param1, param2, two_op): if op not in AVAILABLE_OPS: raise Exception('Invalid operation!') if op in ONEARG_OPS and two_op: # we received two params (two_op = True) raise Exception('Wrong number of arguments: This is a ONE argument operation.') if op in TWOARG_OPS and (not two_op): # we received one param (two_op = False) raise Exception('Wrong number of arguments: This is a TWO argument operation.') # translate copy and move operations if op == 'cp': # this means local to server if 'ftp' in param2: op = 'cp-lcl-server' # otherwise, server to local else: op = 'cp-server-lcl' elif op == 'mv': if 'ftp' in param2: op = 'mv-lcl-server' else: op = 'mv-server-lcl' # operation mappings OPS = { 'ls' : ['LIST'], 'mkdir' : ['MKD'], 'rm' : ['DELE'], 'rmdir' : ['RMD'], 'cp-lcl-server' : ['STOR'], 'cp-server-lcl' : ['RETR'], 'mv-lcl-server' : ['STOR', 'DELE'], # ensures we delete file when executing move 'mv-server-lcl' : ['RETR', 'DELE'] # ensures we delete file when executing move } return OPS[op] # sends commands to the server def send_command(s, cmd, param): try: if param != '': param = ' ' + param s.sendall(bytes(cmd + param + '\r\n', 'utf-8')) except s.error as err: print('Error: Unable to send the server command.' + err) # receives responses from the server def receive_server_response(s): try: response = s.recv(1024).decode() # check that there is still data coming (len > 0) while (len(response) > 0) and (not '\r\n' in response): response += s.recv(1024).decode() print(response) return response except s.error as err: print('Error: Unable to receive the server response.' + err) # login with parsed user and password def attempt_login(csock): # first, send user command send_command(csock, 'USER', USER) s_response = receive_server_response(csock) # now, check if server is expecting password if '3' == str(s_response)[0]: send_command(csock, 'PASS', PSWD) receive_server_response(csock) # sets necessary properties before # attempting to upload or download def before_upload_download_setup(csock): # set the connection to 8-bit binary data mode send_command(csock, 'TYPE I', '') receive_server_response(csock) # set the connection to stream mode send_command(csock, 'MODE S', '') receive_server_response(csock) # set the connection to file-oriented mode send_command(csock, 'STRU F', '') receive_server_response(csock) # retrieves the info for TCP/IP socket def retrieve_tcp_info(resp): if resp.startswith('227 Entering Passive Mode'): connection_numbers = resp[resp.index('(')+1:resp.index(')')] ipandport = connection_numbers.split(",") pstart, pend = int(ipandport[4]), int(ipandport[5]) # first four numbers are the IP address IP = '.'.join(ipandport[0:4]) # last two numbers are port DPORT = (pstart << 8) + pend return IP, DPORT else: raise Exception('Could not retrieve TCP/IP information.') # handles higher-level operation execution # for operations with a data channel def data_channel_op(csock, ops, to_server): # call function to set necessary properties before_upload_download_setup(csock) # execute each operation (could be multiple) for op in ops: execute_operation(csock, op, PATH, lpath, to_server) # carry out the given data channel operation def execute_operation(csock, op, PATH, lpath, to_server): # delete operation if op == 'DELE': if to_server: # make sure to remove from local os.remove(lpath) else: send_command(csock, 'DELE', PATH) receive_server_response(csock) # ask server to open data channel # send PASV and retrieve correct TCP/IP socket send_command(csock, 'PASV', '') resp = receive_server_response(csock) IP, DPORT = retrieve_tcp_info(resp) # after retrieving info for the data channel, # send the operation we want through control channel send_command(csock, op, PATH) # set up our data socket with the given IP and port dsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) dsock.connect((IP, DPORT)) receive_server_response(csock) # send/receive data through data channel # until operation complete # file download if op == 'RETR': with open(lpath, 'wb') as dfile: while True: content = dsock.recv(1024) if not content: break dfile.write(content) dfile.close() receive_server_response(dsock) # file upload elif op == 'STOR': content = '' with open(lpath, 'rb') as nfile: content = nfile.read() dsock.sendall(content) # client closes data socket in this case dsock.close() return elif op == 'LIST': # is there actually any contents to list? contents = False try: resp = receive_server_response(dsock) if (len(resp) != 0): contents = True except Exception as err: print('Error: could not list' + err) quit() if contents == False: print('There are no contents to list at this path.') quit() # close data channel dsock.close() if __name__ == '__main__': # argument parser to take in operation and parameters parser = argparse.ArgumentParser( usage="./3700ftp [operation] [param1] [param2]") parser.add_argument('operation', type=str, help='the operation to execute') parser.add_argument('param1', type=str, help='first parameter (path and/or URL) for operation') parser.add_argument('param2', type=str, nargs='?', help='second parameter (OPTIONAL) (path and/or URL) for operation') args = parser.parse_args() # first, retrieve necessary info # before connecting socket to_server = False lpath = '' # if second parameter, check whether # ARG1 is a local file or a URL # and set flag to know whether local -> server | server -> local if args.param2: try: if 'ftp' in args.param1: lpath = args.param2 parse_url_info(args.param1) elif 'ftp' in args.param2: to_server = True lpath = args.param1 parse_url_info(args.param2) else: raise Exception('One argument must be a URL!') except Exception as err: print('File format is not correct.' + err) else: parse_url_info(args.param1) # validate the given operation if args.param2 is not None: ops = check_operation(args.operation, args.param1, args.param2, True) else: ops = check_operation(args.operation, args.param1, '', False) # 1) create first socket csock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2) attempt to connect try: csock.connect((HOST, PORT)) receive_server_response(csock) except socket.error as err_message: print('Unable to connect to server: {}'.format(err_message)) sys.exit(1) # 3) login as user attempt_login(csock) # here, we can use established control channel if args.operation == 'mkdir' or args.operation == 'rm' or args.operation == 'rmdir': send_command(csock, ops[0], PATH) receive_server_response(csock) # else, we need to use data channel (we already validated the operation) else: data_channel_op(csock, ops, to_server) # ask FTP server to close connection send_command(csock, 'QUIT', '') # close socket receive_server_response(csock) csock.close()