262 lines
8.7 KiB
Python
262 lines
8.7 KiB
Python
# Author: Minh Tran and Angelo Reoligio
|
|
# Date: November 30, 2023
|
|
# Description: FTP client (both UDP and TCP implemented)
|
|
|
|
|
|
from socket import socket, AF_INET, SOCK_DGRAM
|
|
from typing import Pattern, Tuple
|
|
from argparse import ArgumentParser
|
|
import os
|
|
import pickle
|
|
import re
|
|
|
|
|
|
# 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]+$")
|
|
|
|
# opcodes
|
|
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"
|
|
|
|
# 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):
|
|
|
|
# server cannot be reached, stop the client immediately
|
|
if not self.pong_received:
|
|
return
|
|
|
|
client_socket = socket(AF_INET, SOCK_DGRAM)
|
|
|
|
try:
|
|
while True:
|
|
# get command from user
|
|
command = input(f"myftp> - {self.mode} - : ").strip().lower()
|
|
|
|
# handling the "bye" command
|
|
if command == "bye":
|
|
client_socket.close()
|
|
print(f"myftp> - {self.mode} - Session is terminated")
|
|
break
|
|
|
|
# list files available on the server
|
|
elif command == "list":
|
|
self.get_files_list_from_server(client_socket)
|
|
continue
|
|
|
|
# help
|
|
elif command == "help":
|
|
request_payload: str = help_requrest_opcode + "00000"
|
|
print(
|
|
f"myftp> - {self.mode} - : asking for help from the server"
|
|
) if self.debug else None
|
|
|
|
# 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
|
|
|
|
# change command handling
|
|
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
|
|
|
|
client_socket.sendto(request_payload.encode("utf-8"), (self.server_name, self.server_port))
|
|
|
|
modified_message = client_socket.recv(2048)[1:]
|
|
|
|
print(modified_message.decode())
|
|
|
|
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()
|
|
|
|
# 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(
|
|
f"myftp> - {self.mode} - Server at {self.server_name}:{self.server_port} is valid. Response received: {data.decode('utf-8')}"
|
|
)
|
|
|
|
# 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()
|
|
|
|
# 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
|
|
|
|
|
|
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:
|
|
raise ValueError
|
|
|
|
# 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(
|
|
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."
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
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)",
|
|
)
|
|
|
|
arg_parser.add_argument(
|
|
"--directory", required=True, type=str, help="Path to the client directory"
|
|
)
|
|
|
|
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")
|
|
|
|
if not check_directory(args.directory):
|
|
print(
|
|
f"The directory '{args.directory}' does not exists or is not readable/writable."
|
|
)
|
|
return
|
|
|
|
# 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()
|