File-Transport-Protocol-Client / 3700ftp.py
3700ftp.py
Raw
#!/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()