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-11-29 00:00:51 -05:00
from typing import Pattern , Tuple
2023-11-26 16:09:05 -05:00
from argparse import ArgumentParser
2023-11-27 14:19:19 -05:00
import os
2023-11-27 15:28:54 -05:00
import pickle
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-01 04:04:18 -05:00
put_request_opcode : str = " 000 "
get_request_opcode : str = " 001 "
change_request_opcode : str = " 010 "
summary_request_opcode : str = " 011 "
help_requrest_opcode : str = " 100 "
# Res-code
correct_put_and_change_request_rescode : str = " 000 "
correct_get_request_rescode : str = " 001 "
correct_summary_request_rescode : str = " 010 "
file_not_error_rescode : str = " 011 "
unknown_request_rescode : str = " 100 "
unsuccessful_change_rescode : str = " 101 "
help_rescode : str = " 110 "
2023-11-30 15:49:41 -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 ]
class UDPClient :
def __init__ ( self , server_name : str , server_port : int , debug : bool ) :
self . server_name : str = server_name
self . server_port : int = server_port
self . mode : str = " UDP "
self . pong_received : bool = False
self . debug = debug
def run ( self ) :
2023-11-30 14:53:19 -05:00
2023-11-26 16:09:05 -05:00
# server cannot be reached, stop the client immediately
if not self . pong_received :
return
2023-12-01 05:47:53 -05:00
client_socket = socket ( AF_INET , SOCK_DGRAM )
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-11-29 00:00:51 -05:00
command = input ( f " myftp> - { self . mode } - : " ) . 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 ( )
print ( f " myftp> - { self . mode } - Session is terminated " )
break
2023-11-29 00:00:51 -05:00
# list files available on the server
2023-11-27 15:28:54 -05:00
elif command == " list " :
2023-11-30 15:35:41 -05:00
self . get_files_list_from_server ( client_socket )
2023-11-27 15:28:54 -05:00
continue
2023-11-29 00:00:51 -05:00
# help
elif command == " help " :
2023-12-01 04:04:18 -05:00
request_payload : str = help_requrest_opcode + " 00000 "
print (
f " myftp> - { self . mode } - : asking for help from the server "
) if self . debug else None
2023-11-29 00:00:51 -05:00
# get command handling
elif get_command_pattern . match ( command ) :
_ , filename = command . split ( " " , 1 )
print (
f " myftp> - { self . mode } - : getting file { filename } from the server "
) if self . debug else None
# put command handling
elif put_command_pattern . match ( command ) :
_ , filename = command . split ( " " , 1 )
print (
f " myftp> - { self . mode } - : putting file { filename } into the server "
) if self . debug else None
# summary command handling
elif summary_command_pattern . match ( command ) :
_ , filename = command . split ( " " , 1 )
print (
f " myftp> - { self . mode } - : summary file { filename } from the server "
) if self . debug else None
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 ) :
_ , old_filename , new_filename = command . split ( )
print (
f " myftp> - { self . mode } - : changing file named { old_filename } into { new_filename } on the server "
) if self . debug else None
else :
print (
f " myftp> - { self . mode } - : Invalid command. Supported commands are put, get, summary, change, list and help. Type help for detailed usage. "
)
continue
2023-12-01 05:47:53 -05:00
client_socket . sendto ( request_payload . encode ( " utf-8 " ) , ( self . server_name , self . server_port ) )
2023-12-01 04:04:18 -05:00
modified_message = client_socket . recv ( 2048 ) [ 1 : ]
2023-12-01 05:47:53 -05:00
2023-11-27 00:21:09 -05:00
print ( modified_message . decode ( ) )
2023-12-01 05:47:53 -05:00
except ConnectionRefusedError :
print (
f " myftp> - { self . mode } - ConnectionRefusedError happened. Please restart the client program, make sure the server is running and/or put a different server name and server port. "
)
except Exception as error :
print (
f " myftp> - { self . mode } - { error } happened. "
)
finally :
client_socket . close ( )
2023-11-26 16:09:05 -05:00
# ping pong UDP
def check_udp_server ( self ) :
# Create a UDP socket
client_socket = socket ( AF_INET , SOCK_DGRAM )
# will time out after 5 seconds
client_socket . settimeout ( 5 )
try :
# Send a test message to the server
message = b " ping "
client_socket . sendto ( message , ( self . server_name , self . server_port ) )
# Receive the response
data , _ = client_socket . recvfrom ( 1024 )
# If the server responds, consider the address valid
print (
2023-11-27 00:21:09 -05:00
f " myftp> - { self . mode } - Server at { self . server_name } : { self . server_port } is valid. Response received: { data . decode ( ' utf-8 ' ) } "
2023-11-26 16:09:05 -05:00
)
# code reached here meaning no problem with the connection
self . pong_received = True
except TimeoutError :
# Server did not respond within the specified timeout
print (
f " myftp> - { self . mode } - Server at { self . server_name } did not respond within 5 seconds. Check the address or server status. "
)
finally :
# Close the socket
client_socket . close ( )
2023-11-30 15:35:41 -05:00
# get list of files currently on the server
def get_files_list_from_server ( self , client_socket : socket ) - > list [ str ] :
client_socket . send ( " list " . encode ( ) )
encoded_message , server_address = client_socket . recvfrom ( 4096 )
file_list = pickle . loads ( encoded_message )
print ( f " Received file list from { server_address } : { file_list } " )
client_socket . close ( )
return file_list
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
except ValueError as e :
print (
2023-12-01 04:04:18 -05:00
f " 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-11-26 16:09:05 -05:00
# UDP client selected here
if protocol_selection == " 2 " :
user_supplied_address = get_address_input ( )
udp_client = UDPClient (
user_supplied_address [ 0 ] , user_supplied_address [ 1 ] , args . debug
)
udp_client . check_udp_server ( )
udp_client . run ( )
else :
# tcp client here
pass
if __name__ == " __main__ " :
init ( )