2023-11-30 14:41:52 -05:00
# Author: Minh Tran and Angelo Reoligio
# Date: November 30, 2023
# Description: FTP client (both UDP and TCP implemented)
2023-11-26 16:09:05 -05:00
from socket import socket , AF_INET , SOCK_DGRAM
2023-12-07 00:38:33 -05:00
from typing import Pattern , Tuple , Optional
2023-11-26 16:09:05 -05:00
from argparse import ArgumentParser
2023-12-06 17:30:12 -05:00
import traceback
2023-11-27 14:19:19 -05:00
import os
2023-11-29 00:00:51 -05:00
import re
2023-11-30 15:49:41 -05:00
2023-11-29 00:00:51 -05:00
# patterns for command matchings
# compiled for extra performance
get_command_pattern : Pattern = re . compile ( r " ^get \ s+[^ \ s]+$ " )
put_command_pattern : Pattern = re . compile ( r " ^put \ s+[^ \ s]+$ " )
summary_command_pattern : Pattern = re . compile ( r " ^summary \ s+[^ \ s]+$ " )
change_command_pattern : Pattern = re . compile ( r " ^change \ s+[^ \ s]+ \ s+[^ \ s]+$ " )
2023-11-26 16:09:05 -05:00
2023-11-30 15:49:41 -05:00
# opcodes
2023-12-06 17:30:12 -05:00
put_request_opcode : int = 0b000
get_request_opcode : int = 0b001
change_request_opcode : int = 0b010
summary_request_opcode : int = 0b011
help_request_opcode : int = 0b100
2023-12-06 23:03:36 -05:00
unknown_request_opcode : int = 0b101
2023-12-01 04:04:18 -05:00
2023-12-01 07:30:56 -05:00
# Res-code dict
2023-12-06 17:30:12 -05:00
rescode_dict : dict [ int , str ] = {
0b011 : " File Not Found Error " ,
0b100 : " Unknown Request " ,
2023-12-07 00:38:33 -05:00
0b101 : " Change/Put Unsuccessful Error " ,
2023-12-06 17:30:12 -05:00
0b000 : " Put/Change Request Successful " ,
0b001 : " Get Request Successful " ,
0b010 : " Summary Request Successful " ,
0b110 : " Help " ,
2023-12-01 07:30:56 -05:00
}
2023-11-30 15:49:41 -05:00
2023-12-06 17:30:12 -05:00
2023-11-26 16:09:05 -05:00
# custome type to represent the hostname(server name) and the server port
Address = Tuple [ str , int ]
2023-12-06 17:30:12 -05:00
class Client :
def __init__ (
self ,
server_name : str ,
server_port : int ,
directory_path : str ,
debug : bool ,
protocol : str ,
) :
2023-11-26 16:09:05 -05:00
self . server_name : str = server_name
self . server_port : int = server_port
2023-12-06 17:30:12 -05:00
self . protocol : str = protocol
self . directory_path = directory_path
2023-11-26 16:09:05 -05:00
self . debug = debug
def run ( self ) :
2023-12-01 05:47:53 -05:00
client_socket = socket ( AF_INET , SOCK_DGRAM )
2023-12-06 17:30:12 -05:00
client_socket . settimeout ( 10 )
2023-11-27 14:19:19 -05:00
2023-12-01 05:47:53 -05:00
try :
while True :
2023-11-27 00:21:09 -05:00
# get command from user
2023-12-06 17:30:12 -05:00
command = input ( f " myftp> - { self . protocol } - : " ) . strip ( ) . lower ( )
2023-11-27 00:21:09 -05:00
2023-11-27 04:24:47 -05:00
# handling the "bye" command
2023-11-27 00:21:09 -05:00
if command == " bye " :
client_socket . close ( )
2023-12-06 17:30:12 -05:00
print ( f " myftp> - { self . protocol } - Session is terminated " )
2023-11-27 00:21:09 -05:00
break
2023-11-29 00:00:51 -05:00
# help
elif command == " help " :
2023-12-06 17:30:12 -05:00
first_byte : int = help_request_opcode << 5
command_name = " help "
2023-12-01 04:04:18 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Asking for help from the server "
2023-12-01 04:04:18 -05:00
) if self . debug else None
2023-11-29 00:00:51 -05:00
# get command handling
elif get_command_pattern . match ( command ) :
2023-12-06 17:30:12 -05:00
command_name , filename = command . split ( " " , 1 )
first_byte = ( get_request_opcode << 5 ) + len ( filename )
2023-12-07 00:38:33 -05:00
second_byte_to_n_byte = filename . encode ( " ascii " )
2023-12-06 17:30:12 -05:00
2023-11-29 00:00:51 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Getting file { filename } from the server "
2023-11-29 00:00:51 -05:00
) if self . debug else None
# put command handling
elif put_command_pattern . match ( command ) :
2023-12-06 23:03:36 -05:00
command_name , filename = command . split ( " " , 1 )
2023-12-07 00:38:33 -05:00
first_byte , second_byte_to_n_byte , data = self . put_payload_handling (
filename
)
2023-11-29 00:00:51 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Putting file { filename } into the server "
2023-11-29 00:00:51 -05:00
) if self . debug else None
# summary command handling
elif summary_command_pattern . match ( command ) :
2023-12-06 23:03:36 -05:00
command_name , filename = command . split ( " " , 1 )
2023-11-29 00:00:51 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Summary file { filename } from the server "
2023-11-29 00:00:51 -05:00
) if self . debug else None
2023-12-07 17:44:08 -05:00
first_byte = ( summary_request_opcode << 5 ) + len ( filename )
second_byte_to_n_byte = filename . encode ( " ascii " )
2023-12-01 04:04:18 -05:00
# change command handling
2023-11-29 00:00:51 -05:00
elif change_command_pattern . match ( command ) :
2023-12-06 23:03:36 -05:00
command_name , old_filename , new_filename = command . split ( )
2023-12-07 20:27:10 -05:00
2023-11-29 00:00:51 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Changing file named { old_filename } into { new_filename } on the server "
2023-11-29 00:00:51 -05:00
) if self . debug else None
2023-12-07 20:27:10 -05:00
first_byte = ( change_request_opcode << 5 ) + len ( old_filename )
second_byte_to_n_byte = (
old_filename . encode ( " ascii " )
+ len ( new_filename ) . to_bytes ( 1 , " big " )
+ new_filename . encode ( " ascii " )
)
2023-12-06 23:03:36 -05:00
# unknown request, assigned opcode is 0b101
2023-11-29 00:00:51 -05:00
else :
2023-12-06 23:03:36 -05:00
command_name = None
first_byte : int = unknown_request_opcode << 5
2023-11-29 00:00:51 -05:00
2023-12-07 20:27:10 -05:00
# get change put cases
if (
command_name == " get "
or command_name == " summary "
or command_name == " change "
) :
2023-12-06 23:03:36 -05:00
payload = first_byte . to_bytes ( 1 , " big " ) + second_byte_to_n_byte # type: ignore
2023-12-07 00:38:33 -05:00
elif command_name == " put " :
payload = (
first_byte . to_bytes ( 1 , " big " ) + second_byte_to_n_byte + data # type: ignore
if second_byte_to_n_byte is not None and data is not None # type: ignore
else first_byte . to_bytes ( 1 , " big " ) # type: ignore
)
2023-12-06 23:03:36 -05:00
# help case and unknown request
2023-12-06 17:30:12 -05:00
else :
2023-12-06 23:03:36 -05:00
payload : bytes = first_byte . to_bytes ( 1 , " big " ) # type: ignore
2023-12-06 17:30:12 -05:00
print (
2023-12-06 23:03:36 -05:00
f " myftp> - { self . protocol } - sent payload { bin ( int . from_bytes ( payload , byteorder = ' big ' ) ) [ 2 : ] } to the server " # type: ignore
2023-12-06 17:30:12 -05:00
) if self . debug else None
2023-12-01 16:51:11 -05:00
2023-12-06 23:03:36 -05:00
client_socket . sendto ( payload , ( self . server_name , self . server_port ) ) # type: ignore
2023-12-01 05:47:53 -05:00
2023-12-01 07:30:56 -05:00
response_payload = client_socket . recv ( 2048 )
2023-12-01 05:47:53 -05:00
2023-12-01 07:30:56 -05:00
self . parse_response_payload ( response_payload )
2023-12-01 05:47:53 -05:00
except ConnectionRefusedError :
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - ConnectionRefusedError happened. Please restart the client program, make sure the server is running and/or put a different server name and server port. "
2023-11-26 16:09:05 -05:00
)
except TimeoutError :
# Server did not respond within the specified timeout
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Server at { self . server_name } did not respond within 5 seconds. Check the address or server status. "
2023-11-26 16:09:05 -05:00
)
2023-12-06 17:30:12 -05:00
except Exception as error :
traceback_info = traceback . format_exc ( )
2023-11-26 16:09:05 -05:00
2023-12-06 17:30:12 -05:00
print ( f " myftp> - { self . protocol } - { error } happened. " )
2023-11-30 15:35:41 -05:00
2023-12-06 17:30:12 -05:00
print ( traceback_info )
2023-11-30 15:35:41 -05:00
2023-12-06 17:30:12 -05:00
finally :
client_socket . close ( )
2023-12-01 07:30:56 -05:00
2023-12-06 17:30:12 -05:00
def parse_response_payload ( self , response_payload : bytes ) :
first_byte = bytes ( [ response_payload [ 0 ] ] )
first_byte_binary = int . from_bytes ( first_byte , " big " )
rescode = first_byte_binary >> 5
filename_length = first_byte_binary & 0b00011111
response_data = response_payload [ 1 : ]
response_data_length = len ( response_data )
2023-12-01 07:30:56 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - First_byte from server response: { first_byte } . Rescode: { rescode } . File name length: { filename_length } . Data length: { response_data_length } "
2023-12-01 07:30:56 -05:00
) if self . debug else None
try :
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Res-code meaning: { rescode_dict [ rescode ] } "
2023-12-01 07:30:56 -05:00
) if self . debug else None
except KeyError :
2023-12-06 17:30:12 -05:00
print ( f " myftp> - { self . protocol } - Res-code does not have meaning " )
# error rescodes
if rescode in [ 0b011 , 0b100 , 0b101 ] :
print ( f " myftp> - { self . protocol } - { rescode_dict [ rescode ] } " )
# successful rescodes
else :
2023-12-07 00:38:33 -05:00
# help rescode and successful change or put rescode
2023-12-06 17:30:12 -05:00
if rescode == 0b110 :
print ( f " myftp> - { self . protocol } - { response_data . decode ( ' ascii ' ) } " )
2023-12-07 00:38:33 -05:00
elif rescode == 0b000 :
print ( f " myftp> - { self . protocol } - { rescode_dict [ rescode ] } " )
# get rescode
elif rescode == 0b001 :
2023-12-06 17:30:12 -05:00
self . handle_get_response_from_server ( filename_length , response_data )
2023-12-07 00:38:33 -05:00
def put_payload_handling (
self , filename : str
) - > Tuple [ int , Optional [ bytes ] , Optional [ bytes ] ] :
"""
Assemble the pay load to put the file onto server
Return first_byte , second_byte_to_n_byte and data if successful
Or ( None , None , None ) if file not found
"""
try :
with open ( os . path . join ( self . directory_path , filename ) , " rb " ) as file :
content = file . read ( )
content_length = len ( content )
first_byte = ( put_request_opcode << 5 ) + len ( filename )
second_byte_to_n_byte = filename . encode (
" ascii "
) + content_length . to_bytes ( 4 , " big " )
data = content
return ( first_byte , second_byte_to_n_byte , data )
except FileNotFoundError :
return ( ( put_request_opcode << 5 ) , None , None )
2023-12-06 17:30:12 -05:00
def handle_get_response_from_server (
self , filename_length : int , response_data : bytes
) :
"""
2023-12-07 00:38:33 -05:00
Response_data is
File name ( filename_length bytes ) +
File size ( 4 bytes ) +
File content ( rest of the bytes )
2023-12-06 17:30:12 -05:00
"""
try :
filename = response_data [ : filename_length ] . decode ( " ascii " )
file_size = int . from_bytes (
response_data [ filename_length : filename_length + 4 ] , " big "
)
file_content = response_data [
filename_length + 4 : filename_length + 4 + file_size
]
2023-12-01 07:30:56 -05:00
print (
2023-12-06 17:30:12 -05:00
f " myftp> - { self . protocol } - Filename: { filename } , File_size: { file_size } bytes "
2023-12-01 07:30:56 -05:00
)
2023-12-06 17:30:12 -05:00
with open ( os . path . join ( self . directory_path , filename ) , " wb " ) as file :
file . write ( file_content )
print (
f " myftp> - { self . protocol } - File { filename } has been downloaded successfully "
)
except Exception :
raise
2023-12-01 07:30:56 -05:00
2023-11-26 16:09:05 -05:00
def get_address_input ( ) - > Address :
while True :
try :
# Get input as a space-separated string
input_string = input ( " myftp>Provide IP address and Port number \n " )
# Split the input into parts
input_parts = input_string . split ( )
# Ensure there are exactly two parts
if len ( input_parts ) != 2 :
2023-12-01 04:04:18 -05:00
raise ValueError
2023-11-26 16:09:05 -05:00
# Extract the values and create the tuple
string_part , int_part = input_parts
address = ( string_part , int ( int_part ) )
# Valid tuple, return it
return address
2023-12-06 17:30:12 -05:00
except ValueError :
2023-11-26 16:09:05 -05:00
print (
2023-12-06 17:30:12 -05:00
" Error: Invalid input. Please enter a servername/hostname/ip address as a string and the port number as an integer separated by a space. "
2023-11-26 16:09:05 -05:00
)
2023-11-27 14:19:19 -05:00
def check_directory ( path ) :
if os . path . exists ( path ) :
if os . path . isdir ( path ) :
if os . access ( path , os . R_OK ) and os . access ( path , os . W_OK ) :
return True
else :
print ( f " Error: The directory ' { path } ' is not readable or writable. " )
else :
print ( f " Error: ' { path } ' is not a directory. " )
else :
print ( f " Error: The directory ' { path } ' does not exist. " )
return False
2023-11-26 16:09:05 -05:00
def init ( ) :
arg_parser = ArgumentParser ( description = " A FTP client written in Python " )
arg_parser . add_argument (
" --debug " ,
type = int ,
choices = [ 0 , 1 ] ,
default = 0 ,
required = False ,
help = " Enable or disable the flag (0 or 1) " ,
)
2023-11-27 14:19:19 -05:00
arg_parser . add_argument (
" --directory " , required = True , type = str , help = " Path to the client directory "
)
2023-11-26 16:09:05 -05:00
args = arg_parser . parse_args ( )
while (
protocol_selection := input ( " myftp>Press 1 for TCP, Press 2 for UDP \n " )
) not in { " 1 " , " 2 " } :
print ( " myftp>Invalid choice. Press 1 for TCP, Press 2 for UDP " )
2023-11-27 14:19:19 -05:00
if not check_directory ( args . directory ) :
print (
f " The directory ' { args . directory } ' does not exists or is not readable/writable. "
)
return
2023-12-06 17:30:12 -05:00
user_supplied_address = get_address_input ( )
2023-11-26 16:09:05 -05:00
# UDP client selected here
if protocol_selection == " 2 " :
2023-12-06 17:30:12 -05:00
udp_client = Client (
user_supplied_address [ 0 ] ,
user_supplied_address [ 1 ] ,
args . directory ,
args . debug ,
" UDP " ,
2023-11-26 16:09:05 -05:00
)
udp_client . run ( )
else :
2023-12-06 17:30:12 -05:00
tcp_client = Client (
user_supplied_address [ 0 ] ,
user_supplied_address [ 1 ] ,
args . directory ,
args . debug ,
" TCP " ,
)
tcp_client . run ( )
2023-11-26 16:09:05 -05:00
if __name__ == " __main__ " :
init ( )