2023-11-30 14:41:52 -05:00
|
|
|
# Author: Minh Tran and Angelo Reoligio
|
|
|
|
# Date: November 30, 2023
|
|
|
|
# Description: FTP server (both UDP and TCP implemented)
|
|
|
|
|
|
|
|
|
2023-11-25 22:35:43 -05:00
|
|
|
from socket import socket, AF_INET, SOCK_DGRAM
|
|
|
|
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-25 22:35:43 -05:00
|
|
|
|
2023-11-30 16:39:25 -05:00
|
|
|
# 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-12-01 04:04:18 -05:00
|
|
|
# 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"
|
|
|
|
|
2023-11-25 22:35:43 -05:00
|
|
|
|
|
|
|
class UDPServer:
|
2023-11-27 15:28:54 -05:00
|
|
|
def __init__(
|
|
|
|
self, server_name: str, server_port: int, directory_path: str, debug: bool
|
|
|
|
) -> None:
|
2023-11-25 22:35:43 -05:00
|
|
|
self.server_name = server_name
|
|
|
|
self.server_port = server_port
|
2023-11-27 00:36:04 -05:00
|
|
|
self.mode: str = "UDP"
|
2023-11-27 15:28:54 -05:00
|
|
|
self.directory_path = directory_path
|
2023-11-25 22:35:43 -05:00
|
|
|
self.debug = debug
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self.server_socket = socket(AF_INET, SOCK_DGRAM)
|
|
|
|
self.server_socket.bind((self.server_name, self.server_port))
|
|
|
|
|
|
|
|
print(
|
2023-12-01 07:30:56 -05:00
|
|
|
f"myftp> - {self.mode} - Server is ready to receive at {self.server_name}:{self.server_port}"
|
2023-11-25 22:35:43 -05:00
|
|
|
) if self.debug else None
|
|
|
|
|
|
|
|
shut_down = False
|
|
|
|
|
|
|
|
try:
|
|
|
|
while not shut_down:
|
|
|
|
message, clientAddress = self.server_socket.recvfrom(2048)
|
2023-12-01 16:51:11 -05:00
|
|
|
|
|
|
|
# decode for quick and dirty commands like ping and list server files
|
|
|
|
# outside of the scope of the project
|
|
|
|
try:
|
|
|
|
request_payload = message.decode()
|
|
|
|
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
# most commands (get, put, summary ...) will be handled by this catch block
|
|
|
|
request_payload: str = bin(int.from_bytes(message, byteorder='big'))[2:]
|
2023-11-25 22:35:43 -05:00
|
|
|
|
|
|
|
print(
|
2023-12-01 07:30:56 -05:00
|
|
|
f"myftp> - {self.mode} ------------------------------------------------------------------"
|
|
|
|
) if self.debug else None
|
|
|
|
|
|
|
|
print(
|
|
|
|
f"myftp> - {self.mode} - Received message from client at {clientAddress}: {request_payload}"
|
2023-11-25 22:35:43 -05:00
|
|
|
) if self.debug else None
|
|
|
|
|
2023-11-29 00:00:51 -05:00
|
|
|
# check for connectivity
|
2023-12-01 04:04:18 -05:00
|
|
|
if request_payload == "ping":
|
|
|
|
self.server_socket.sendto("pong".encode(), clientAddress)
|
2023-12-01 07:30:56 -05:00
|
|
|
|
|
|
|
print(
|
|
|
|
f"myftp> - {self.mode} - pong sent back to client"
|
|
|
|
) if self.debug else None
|
|
|
|
|
2023-12-01 04:04:18 -05:00
|
|
|
continue
|
2023-11-27 15:28:54 -05:00
|
|
|
|
2023-11-29 00:00:51 -05:00
|
|
|
# list files available on server
|
2023-12-01 04:04:18 -05:00
|
|
|
elif request_payload == "list":
|
2023-11-27 15:28:54 -05:00
|
|
|
encoded_message = pickle.dumps(
|
|
|
|
get_files_in_directory(self.directory_path)
|
|
|
|
)
|
|
|
|
self.server_socket.sendto(encoded_message, clientAddress)
|
|
|
|
continue
|
|
|
|
|
2023-12-01 04:04:18 -05:00
|
|
|
# help request handling
|
|
|
|
elif request_payload == help_requrest_opcode + "00000":
|
|
|
|
print(
|
2023-12-01 07:30:56 -05:00
|
|
|
f"myftp> - {self.mode} - Client message parsed. Received help request"
|
2023-12-01 04:04:18 -05:00
|
|
|
) if self.debug else None
|
2023-12-01 07:30:56 -05:00
|
|
|
|
2023-12-01 04:04:18 -05:00
|
|
|
rescode = help_rescode
|
|
|
|
response_data_string = "get,put,summary,change,help,bye"
|
|
|
|
|
2023-11-26 16:09:05 -05:00
|
|
|
else:
|
2023-12-01 04:04:18 -05:00
|
|
|
# handle unrecognized request here
|
|
|
|
pass
|
|
|
|
|
|
|
|
payload: bytes = self.build_res_payload(rescode, response_data_string)
|
|
|
|
|
|
|
|
self.server_socket.sendto(payload, clientAddress)
|
2023-11-26 16:09:05 -05:00
|
|
|
|
2023-11-27 00:36:04 -05:00
|
|
|
print(
|
2023-12-01 07:30:56 -05:00
|
|
|
f"myftp> - {self.mode} - Sent message to client at {clientAddress}: {payload}"
|
2023-11-27 00:36:04 -05:00
|
|
|
) if self.debug else None
|
|
|
|
|
2023-11-25 22:35:43 -05:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
shut_down = True
|
|
|
|
self.server_socket.close()
|
2023-12-01 07:30:56 -05:00
|
|
|
print(f"myftp> - {self.mode} - Server shutting down")
|
2023-11-25 22:35:43 -05:00
|
|
|
|
|
|
|
finally:
|
2023-12-01 07:30:56 -05:00
|
|
|
print(f"myftp> - {self.mode} - Closed the server socket")
|
2023-11-25 22:35:43 -05:00
|
|
|
|
2023-12-01 04:04:18 -05:00
|
|
|
# assembling the payload to send back to the client
|
|
|
|
def build_res_payload(self,
|
2023-12-01 07:30:56 -05:00
|
|
|
rescode: str,
|
|
|
|
response_data_string: str) -> bytes:
|
|
|
|
|
|
|
|
print(f"myftp> - {self.mode} - Assembling response payload to be sent back to the client")
|
2023-12-01 04:04:18 -05:00
|
|
|
|
|
|
|
bytes_response_data = response_data_string.encode("utf-8")
|
|
|
|
|
|
|
|
data_len = len(bytes_response_data)
|
|
|
|
|
|
|
|
print(f"myftp> - {self.mode} - Rescode {rescode}") if self.debug else None
|
|
|
|
print(f"myftp> - {self.mode} - Length of data {data_len}") if self.debug else None
|
|
|
|
print(f"myftp> - {self.mode} - Data {response_data_string}") if self.debug else None
|
|
|
|
|
|
|
|
# convert to binary
|
|
|
|
try:
|
|
|
|
# pad the length of data to make sure it is always 5 bits
|
|
|
|
# i.e "010" -> "00010"
|
|
|
|
binary_data_len: str = bin(data_len).zfill(5)
|
|
|
|
|
|
|
|
print(f"myftp> - {self.mode} - binary_data_len {binary_data_len[2:]}") if self.debug else None
|
|
|
|
|
|
|
|
# create the first byte
|
|
|
|
# since binary_data_len is of the format 0b00100, we have to remove the first two characters 0b
|
|
|
|
first_byte: bytes = bytes([int(rescode + binary_data_len[2:], 2)])
|
|
|
|
|
|
|
|
print(f"myftp> - {self.mode} - First byte assembled for rescode {rescode}: {bin(int.from_bytes(first_byte, byteorder='big'))[2:]}") if self.debug else None
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception(e)
|
|
|
|
|
|
|
|
res_payload = first_byte + bytes_response_data
|
|
|
|
|
|
|
|
return res_payload
|
|
|
|
|
2023-11-25 22:35:43 -05:00
|
|
|
|
2023-11-27 15:28:54 -05:00
|
|
|
def get_files_in_directory(directory_path: str) -> list[str]:
|
|
|
|
file_list = []
|
2023-11-29 00:00:51 -05:00
|
|
|
for _, _, files in os.walk(directory_path):
|
2023-11-27 15:28:54 -05:00
|
|
|
for file in files:
|
|
|
|
file_list.append(file)
|
|
|
|
return file_list
|
|
|
|
|
|
|
|
|
|
|
|
def check_directory(path: str) -> bool:
|
2023-11-27 14:19:19 -05:00
|
|
|
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-25 22:35:43 -05:00
|
|
|
def init():
|
2023-11-26 16:09:05 -05:00
|
|
|
parser = ArgumentParser(description="A FTP server written in Python")
|
2023-11-25 22:35:43 -05:00
|
|
|
|
|
|
|
parser.add_argument(
|
2023-11-26 16:20:26 -05:00
|
|
|
"--port_number",
|
|
|
|
default=12000,
|
|
|
|
required=False,
|
|
|
|
type=int,
|
|
|
|
help="Port number for the server. Default = 12000",
|
2023-11-25 22:35:43 -05:00
|
|
|
)
|
|
|
|
|
2023-11-27 14:19:19 -05:00
|
|
|
parser.add_argument(
|
|
|
|
"--directory", required=True, type=str, help="Path to the server directory"
|
|
|
|
)
|
|
|
|
|
2023-11-27 04:24:47 -05:00
|
|
|
parser.add_argument(
|
|
|
|
"--ip_addr",
|
|
|
|
default="0.0.0.0",
|
|
|
|
required=False,
|
|
|
|
type=str,
|
|
|
|
help="Port number for the server. Default = 12000",
|
|
|
|
)
|
|
|
|
|
2023-11-25 22:35:43 -05:00
|
|
|
parser.add_argument(
|
|
|
|
"--debug",
|
|
|
|
type=int,
|
|
|
|
choices=[0, 1],
|
|
|
|
default=0,
|
|
|
|
help="Enable or disable the flag (0 or 1)",
|
|
|
|
)
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
2023-11-26 16:09:05 -05:00
|
|
|
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":
|
2023-11-27 15:28:54 -05:00
|
|
|
udp_server = UDPServer(
|
|
|
|
args.ip_addr, args.port_number, args.directory, args.debug
|
|
|
|
)
|
2023-11-26 16:09:05 -05:00
|
|
|
|
|
|
|
udp_server.run()
|
2023-11-25 22:35:43 -05:00
|
|
|
|
2023-11-26 16:09:05 -05:00
|
|
|
else:
|
|
|
|
# tcp client here
|
|
|
|
pass
|
2023-11-25 22:35:43 -05:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
init()
|