feat(udp): get command finished

- Files size should be around 750 bytes or less
- Regenerate test files in docker and in `client_directory` & `server_directory` folders
- Abstract the client and server: UDPClient -> Client
This commit is contained in:
minhtrannhat 2023-12-06 17:30:12 -05:00
parent 5519be9222
commit dcd8c1a1a5
Signed by: minhtrannhat
GPG Key ID: E13CFA85C53F8062
9 changed files with 318 additions and 207 deletions

View File

@ -9,8 +9,10 @@ WORKDIR /client
# Copy the local client to the container
RUN mkdir /client_directory
RUN dd if=/dev/urandom of=/client_directory/file_local.txt bs=1024 count=10
RUN dd if=/dev/urandom of=/client_directory/image_local.png bs=1024 count=50
# these files is only 750 bytes
RUN dd if=/dev/urandom of=/client_directory/file_local.txt bs=1 count=750
RUN dd if=/dev/urandom of=/client_directory/image_local.png bs=1 count=750
# Start your Python application
#CMD python client.py --debug 1

View File

@ -9,8 +9,9 @@ WORKDIR /server
# Create files with random content in the /server directory
RUN mkdir /server_directory
RUN dd if=/dev/urandom of=/server_directory/file_server.txt bs=1024 count=10
RUN dd if=/dev/urandom of=/server_directory/image_server.png bs=1024 count=50
# these files is only 750 bytes
RUN dd if=/dev/urandom of=/server_directory/file_server.txt bs=1 count=750
RUN dd if=/dev/urandom of=/server_directory/image_server.png bs=1 count=750
# Start your Python application
#CMD python server.py --port_number 12000 --debug 1

View File

@ -12,7 +12,7 @@ You can run `python3 src/myftp/client.py --directory <insert valid directory tha
### Server
By default, the server IP address or hostname or server name will be `0.0.0.0` (meaning it will bind to all interfaces). The `--port_number` flag, if not specified will be by default `12000`.
By default, the server IP address or hostname or server name will be `0.0.0.0` or `localhost` (meaning it will bind to all interfaces). The `--port_number` flag, if not specified will be by default `12000`.
You can run `python3 src/myftp/server.py --directory <insert valid directory that you have read/write permissions>` to start the server or `python3 src/myftp/server.py --ip_addr <insert ip addr of the server> --port_number <insert port number here> --debug 1 --directory <insert valid directory that you have read/write permissions>` for debugging purposes and to specify the port number.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,8 +6,8 @@
from socket import socket, AF_INET, SOCK_DGRAM
from typing import Pattern, Tuple
from argparse import ArgumentParser
import traceback
import os
import pickle
import re
@ -19,104 +19,119 @@ 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"
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
# Res-code dict
rescode_dict: dict[str, str] = {
"000": "Put/Change Request Successful",
"001": "Get Request Successful",
"010": "Summary Request Successful",
"011": "File Not Found Error",
"100": "Unknown Request",
"101": "Change Unsuccessful Error",
"110": "Help"
rescode_dict: dict[int, str] = {
0b011: "File Not Found Error",
0b100: "Unknown Request",
0b101: "Change Unsuccessful Error",
0b000: "Put/Change Request Successful",
0b001: "Get Request Successful",
0b010: "Summary Request Successful",
0b110: "Help",
}
# 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):
class Client:
def __init__(
self,
server_name: str,
server_port: int,
directory_path: str,
debug: bool,
protocol: str,
):
self.server_name: str = server_name
self.server_port: int = server_port
self.mode: str = "UDP"
self.pong_received: bool = False
self.protocol: str = protocol
self.directory_path = directory_path
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)
client_socket.settimeout(10)
try:
while True:
# get command from user
command = input(f"myftp> - {self.mode} - : ").strip().lower()
command = input(f"myftp> - {self.protocol} - : ").strip().lower()
# handling the "bye" command
if command == "bye":
client_socket.close()
print(f"myftp> - {self.mode} - Session is terminated")
print(f"myftp> - {self.protocol} - 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" # 10000000
first_byte: int = help_request_opcode << 5
command_name = "help"
print(
f"myftp> - {self.mode} - Asking for help from the server"
f"myftp> - {self.protocol} - Asking for help from the server"
) if self.debug else None
# get command handling
elif get_command_pattern.match(command):
_, filename = command.split(" ", 1)
command_name, filename = command.split(" ", 1)
first_byte = (get_request_opcode << 5) + len(filename)
second_byte_to_n_byte: bytes = filename.encode("ascii")
print(
f"myftp> - {self.mode} - : Getting file {filename} from the server"
f"myftp> - {self.protocol} - 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"
f"myftp> - {self.protocol} - 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"
f"myftp> - {self.protocol} - 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"
f"myftp> - {self.protocol} - 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."
f"myftp> - {self.protocol} - Invalid command. Supported commands are put, get, summary, change, list and help. Type help for detailed usage."
)
continue
# convert the payload to bytes so it can be sent to the server
byte_representation_req_payload: bytes = bytes([int(request_payload, 2)])
# get or put case
if command_name == "get" or command_name == "put":
payload = first_byte.to_bytes(1, "big") + second_byte_to_n_byte
client_socket.sendto(byte_representation_req_payload, (self.server_name, self.server_port))
# help case
else:
payload: bytes = first_byte.to_bytes(1, "big")
print(
f"myftp> - {self.protocol} - sent payload {bin(int.from_bytes(payload, byteorder='big'))[2:]} to the server"
) if self.debug else None
client_socket.sendto(payload, (self.server_name, self.server_port))
response_payload = client_socket.recv(2048)
@ -124,83 +139,86 @@ class UDPClient:
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."
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."
)
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."
f"myftp> - {self.protocol} - Server at {self.server_name} did not respond within 5 seconds. Check the address or server status."
)
except Exception as error:
traceback_info = traceback.format_exc()
print(f"myftp> - {self.protocol} - {error} happened.")
print(traceback_info)
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 parse_response_payload(self,
response_payload: bytes):
# we want to get the first byte as a string i.e "01010010"
first_byte: str = bin(response_payload[0])[2:].zfill(8)
rescode: str = first_byte[:3]
response_data_length: int = int(first_byte[-5:], 2)
response_data: bytes = response_payload[1:]
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)
print(
f"myftp> - {self.mode} - First_byte from server response: {first_byte}. Rescode: {rescode}. Data length: {response_data_length}"
f"myftp> - {self.protocol} - First_byte from server response: {first_byte}. Rescode: {rescode}. File name length: {filename_length}. Data length: {response_data_length}"
) if self.debug else None
try:
print(
f"myftp> - {self.mode} - Res-code meaning: {rescode_dict[rescode]}"
f"myftp> - {self.protocol} - Res-code meaning: {rescode_dict[rescode]}"
) if self.debug else None
except KeyError:
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:
if rescode == 0b110:
print(f"myftp> - {self.protocol} - {response_data.decode('ascii')}")
else:
self.handle_get_response_from_server(filename_length, response_data)
def handle_get_response_from_server(
self, filename_length: int, response_data: bytes
):
"""
response_data is
file name (filename_length bytes) +
file size (4 bytes) +
file content (rest of the bytes)
"""
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
]
print(
f"myftp> - {self.mode} - Res-code does not have meaning"
f"myftp> - {self.protocol} - Filename: {filename}, File_size: {file_size} bytes"
)
print(
f"myftp> - {self.mode} - {response_data.decode()}"
)
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
def get_address_input() -> Address:
@ -223,9 +241,9 @@ def get_address_input() -> Address:
# Valid tuple, return it
return address
except ValueError as e:
except ValueError:
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."
"Error: Invalid input. Please enter a servername/hostname/ip address as a string and the port number as an integer separated by a space."
)
@ -272,20 +290,29 @@ def init():
)
return
user_supplied_address = get_address_input()
# 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 = Client(
user_supplied_address[0],
user_supplied_address[1],
args.directory,
args.debug,
"UDP",
)
udp_client.check_udp_server()
udp_client.run()
else:
# tcp client here
pass
tcp_client = Client(
user_supplied_address[0],
user_supplied_address[1],
args.directory,
args.debug,
"TCP",
)
tcp_client.run()
if __name__ == "__main__":

View File

@ -5,33 +5,45 @@
from socket import socket, AF_INET, SOCK_DGRAM
from argparse import ArgumentParser
from typing import Optional, Tuple
import os
import pickle
# 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"
# Res-codes
rescode_success_dict: dict[str, int] = {
"correct_put_and_change_request_rescode": 0b000,
"correct_get_request_rescode": 0b001,
"correct_summary_request_rescode": 0b010,
"help_rescode": 0b110,
}
rescode_fail_dict: dict[str, int] = {
"file_not_error_rescode": 0b011,
"unknown_request_rescode": 0b100,
"unsuccessful_change_rescode": 0b101,
}
# 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"
op_codes_dict: dict[int, str] = {
0b000: "put",
0b001: "get",
0b010: "change",
0b011: "summary",
0b100: "help",
}
class UDPServer:
class Server:
def __init__(
self, server_name: str, server_port: int, directory_path: str, debug: bool
self,
server_name: str,
server_port: int,
directory_path: str,
debug: bool,
protocol: str,
) -> None:
self.server_name = server_name
self.server_port = server_port
self.mode: str = "UDP"
self.protocol: str = protocol
self.directory_path = directory_path
self.debug = debug
@ -40,122 +52,188 @@ class UDPServer:
self.server_socket.bind((self.server_name, self.server_port))
print(
f"myftp> - {self.mode} - Server is ready to receive at {self.server_name}:{self.server_port}"
f"myftp> - {self.protocol} - Server is ready to receive at {self.server_name}:{self.server_port}"
) if self.debug else None
shut_down = False
try:
while not shut_down:
message, clientAddress = self.server_socket.recvfrom(2048)
# 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:]
print(
f"myftp> - {self.mode} ------------------------------------------------------------------"
f"myftp> - {self.protocol} ------------------------------------------------------------------"
) if self.debug else None
req_payload, clientAddress = self.server_socket.recvfrom(2048)
first_byte = bytes([req_payload[0]])
request_type, filename_length = self.decode_first_byte(first_byte)
print(
f"myftp> - {self.mode} - Received message from client at {clientAddress}: {request_payload}"
f"myftp> - {self.protocol} - Received message from client at {clientAddress}: {req_payload}"
) if self.debug else None
# check for connectivity
if request_payload == "ping":
self.server_socket.sendto("pong".encode(), clientAddress)
print(
f"myftp> - {self.mode} - pong sent back to client"
) if self.debug else None
continue
# list files available on server
elif request_payload == "list":
encoded_message = pickle.dumps(
get_files_in_directory(self.directory_path)
)
self.server_socket.sendto(encoded_message, clientAddress)
continue
# help request handling
elif request_payload == help_requrest_opcode + "00000":
if request_type == "help":
print(
f"myftp> - {self.mode} - Client message parsed. Received help request"
f"myftp> - {self.protocol} - Client message parsed. Received help request"
) if self.debug else None
rescode = help_rescode
response_data_string = "get,put,summary,change,help,bye"
rescode = rescode_success_dict["help_rescode"]
response_data = "get,put,summary,change,help,bye".encode("ascii")
filename = None
filename_length = None
else:
# handle unrecognized request here
pass
elif request_type == "get":
pre_payload = self.process_get_req(req_payload[1:])
payload: bytes = self.build_res_payload(rescode, response_data_string)
if (
pre_payload[0] is not None
and pre_payload[1] is not None
and pre_payload[2] is not None
):
rescode = rescode_success_dict["correct_get_request_rescode"]
filename = pre_payload[0]
filename_length = pre_payload[2]
response_data = pre_payload[1]
self.server_socket.sendto(payload, clientAddress)
else:
rescode = rescode_fail_dict["file_not_error_rescode"]
filename_length = None
filename = None
response_data = None
res_payload: bytes = self.build_res_payload(
rescode=rescode, # type: ignore
filename_length=filename_length,
filename=filename, # type: ignore
response_data=response_data, # type:ignore
)
self.server_socket.sendto(res_payload, clientAddress)
print(
f"myftp> - {self.mode} - Sent message to client at {clientAddress}: {payload}"
f"myftp> - {self.protocol} - Sent message to client at {clientAddress}: {res_payload}"
) if self.debug else None
except KeyboardInterrupt:
shut_down = True
self.server_socket.close()
print(f"myftp> - {self.mode} - Server shutting down")
print(f"myftp> - {self.protocol} - Server shutting down")
finally:
print(f"myftp> - {self.mode} - Closed the server socket")
print(f"myftp> - {self.protocol} - Closed the server socket")
def decode_first_byte(self, first_byte: bytes) -> Tuple[str, int]:
"""
Retrieve the request_type from first byte
"""
if len(first_byte) != 1:
raise ValueError("Input is not 1 byte")
first_byte_to_binary = int.from_bytes(first_byte, "big")
try:
request_type = op_codes_dict[first_byte_to_binary >> 5]
filename_length_in_bytes = first_byte_to_binary & 0b00011111
print(
f"myftp> - {self.protocol} - First byte parsed. Request type: {request_type}. Filename length in bytes: {filename_length_in_bytes}"
)
except KeyError:
raise KeyError("Can't find the request type of this payload")
return request_type, filename_length_in_bytes
def process_get_req(
self, second_byte_to_byte_n: bytes
) -> Tuple[Optional[str], Optional[bytes], Optional[int]]:
"""
Process the get request
If successful, return the filename, content and the content_length
If not, return None, None, None tuple
"""
filename = second_byte_to_byte_n.decode("ascii")
try:
with open(os.path.join(self.directory_path, filename), "rb") as file:
content = file.read()
content_length = len(content)
return filename, content, content_length
except FileNotFoundError:
print(f"myftp> - {self.protocol} - file {filename} not found")
return (None, None, None)
# assembling the payload to send back to the client
def build_res_payload(self,
rescode: str,
response_data_string: str) -> bytes:
def build_res_payload(
self,
rescode: int,
filename_length: Optional[int] = None,
filename: Optional[str] = None,
response_data: Optional[bytes] = None,
) -> bytes:
print(
f"myftp> - {self.protocol} - Assembling response payload to be sent back to the client"
)
print(f"myftp> - {self.mode} - Assembling response payload to be sent back to the client")
data_len = len(response_data) if response_data is not None else None
bytes_response_data = response_data_string.encode("utf-8")
print(
f"myftp> - {self.protocol} - Rescode {format(rescode, '03b')}"
) if self.debug else None
data_len = len(bytes_response_data)
print(
f"myftp> - {self.protocol} - Length of data {data_len}"
) if self.debug else None
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
print(
f"myftp> - {self.protocol} - Data {response_data}"
) 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)
# get case
if filename is not None:
first_byte = ((rescode << 5) + len(filename)).to_bytes(1, "big")
# help case
elif filename is None and response_data is not None:
first_byte = ((rescode << 5) + len(response_data)).to_bytes(1, "big")
# unsuccessful cases
else:
first_byte = (rescode << 5).to_bytes(1, "big")
print(f"myftp> - {self.mode} - binary_data_len {binary_data_len[2:]}") if self.debug else None
# we only need the firstbyte
if filename is None:
second_byte_to_FL_plus_five = None
else:
second_byte_to_FL_plus_five = (
filename.encode() + len(response_data).to_bytes(4, "big")
if response_data is not None
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.protocol} - First byte assembled for rescode {format(rescode, '03b')}: {bin(int.from_bytes(first_byte, byteorder='big'))[2:]}"
) if self.debug else None
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
if second_byte_to_FL_plus_five is not None and response_data is not None:
res_payload = first_byte + second_byte_to_FL_plus_five + response_data
# help case
elif second_byte_to_FL_plus_five is None and response_data is not None:
res_payload = first_byte + response_data
else:
res_payload = first_byte
except Exception as e:
raise Exception(e)
return res_payload
res_payload = first_byte + bytes_response_data
return res_payload
def get_files_in_directory(directory_path: str) -> list[str]:
file_list = []
for _, _, files in os.walk(directory_path):
for file in files:
file_list.append(file)
return file_list
except Exception:
raise
def check_directory(path: str) -> bool:
@ -218,15 +296,18 @@ def init():
# UDP client selected here
if protocol_selection == "2":
udp_server = UDPServer(
args.ip_addr, args.port_number, args.directory, args.debug
udp_server = Server(
args.ip_addr, args.port_number, args.directory, args.debug, "UDP"
)
udp_server.run()
else:
# tcp client here
pass
tcp_server = Server(
args.ip_addr, args.port_number, args.directory, args.debug, "TCP"
)
tcp_server.run()
if __name__ == "__main__":