#1916: FTP client STOR request to FTP server

This commit is contained in:
Czar.Echavez
2023-09-20 16:23:35 +01:00
parent f913294058
commit 2e76b3f162
13 changed files with 454 additions and 21 deletions

View File

@@ -27,6 +27,7 @@ class DNSClient(Service):
# TCP for now
kwargs["protocol"] = IPProtocol.TCP
super().__init__(**kwargs)
self.start()
def describe_state(self) -> Dict:
"""

View File

@@ -26,6 +26,7 @@ class DNSServer(Service):
# TCP for now
kwargs["protocol"] = IPProtocol.TCP
super().__init__(**kwargs)
self.start()
def describe_state(self) -> Dict:
"""

View File

@@ -0,0 +1,150 @@
from ipaddress import IPv4Address
from typing import Optional
from primaite.simulator.file_system.file_system import File
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.core.software_manager import SoftwareManager
from primaite.simulator.system.services.service import Service
class FTPClient(Service):
"""
A class for simulating an FTP client service.
This class inherits from the `Service` class and provides methods to emulate FTP
RFC 959: https://datatracker.ietf.org/doc/html/rfc959
"""
connected: bool = False
"""Keeps track of whether or not the FTP client is connected to an FTP server"""
def __init__(self, **kwargs):
kwargs["name"] = "FTPClient"
kwargs["port"] = Port.FTP
kwargs["protocol"] = IPProtocol.TCP
super().__init__(**kwargs)
self.start()
def _connect_to_server(
self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP
) -> bool:
"""
Connects the client to a given FTP server.
:param: dest_ip_address: IP address of the FTP server the client needs to connect to. Optional.
:type: Optional[IPv4Address]
:param: dest_port: Port of the FTP server the client needs to connect to. Optional.
:type: Optional[Port]
:param: server_password: The password to use when connecting to the FTP server. Optional.
:type: Optional[str]
"""
# normally FTP will choose a random port for the transfer, but using the FTP command port will do for now
# create FTP packet
payload: FTPPacket = FTPPacket(
ftp_command=FTPCommand.PORT,
ftp_command_args=Port.FTP,
)
software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager(
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port
)
return payload.status_code == FTPStatusCode.OK
def _disconnect_from_server(
self,
ftp_server_ip_address: Optional[IPv4Address] = None,
) -> bool:
# send a disconnect request payload to FTP server
# return true if connected successfully else false
self.connected = False
def _process_response(self, payload: FTPPacket):
"""
Process any FTPPacket responses.
:param: payload: The FTPPacket payload
:type: FTPPacket
"""
if payload.ftp_command == FTPCommand.PORT:
if payload.status_code == FTPStatusCode.OK:
self.connected = True
def send_file(
self,
dest_ip_address: IPv4Address,
src_folder_name: str,
src_file_name: str,
dest_folder_name: str,
dest_file_name: str,
dest_port: Optional[Port] = Port.FTP,
is_reattempt: Optional[bool] = False,
) -> bool:
"""Send a file to a target IP address."""
file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name)
if not file_to_transfer:
self.sys_log.error(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}")
return False
# check if FTP is currently connected to IP
self.connected = self._connect_to_server(
dest_ip_address=dest_ip_address,
dest_port=dest_port,
)
if not self.connected:
if is_reattempt:
return False
return self.send_file(
src_folder_name=file_to_transfer.folder.name,
src_file_name=file_to_transfer.name,
dest_folder_name=dest_folder_name,
dest_file_name=dest_file_name,
dest_ip_address=dest_ip_address,
dest_port=dest_port,
is_reattempt=True,
)
else:
# send STOR request
payload: FTPPacket = FTPPacket(
ftp_command=FTPCommand.STOR,
ftp_command_args={
"dest_folder_name": dest_folder_name,
"dest_file_name": dest_file_name,
"file_size": file_to_transfer.sim_size,
},
packet_payload_size=file_to_transfer.sim_size,
)
software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager(
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port
)
if payload.status_code == Port.FTP:
self._disconnect_from_server()
return True
def request_file(
self,
dest_ip_address: IPv4Address,
src_folder_name: str,
src_file_name: str,
dest_folder_name: str,
dest_file_name: str,
dest_port: Optional[Port] = Port.FTP,
is_reattempt: Optional[bool] = False,
) -> bool:
"""Request a file from a target IP address."""
pass
def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool:
"""Receives a payload from the SessionManager."""
if not isinstance(payload, FTPPacket):
self.sys_log.error(f"{payload} is not an FTP packet")
return False
self._process_response(payload=payload)
return True

View File

@@ -0,0 +1,71 @@
from typing import Any, Optional
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
from primaite.simulator.network.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.services.service import Service
class FTPServer(Service):
"""
A class for simulating an FTP server service.
This class inherits from the `Service` class and provides methods to emulate FTP
RFC 959: https://datatracker.ietf.org/doc/html/rfc959
"""
server_password: Optional[str] = None
"""Password needed to connect to FTP server. Default is None."""
def __init__(self, **kwargs):
kwargs["name"] = "FTPServer"
kwargs["port"] = Port.FTP
kwargs["protocol"] = IPProtocol.TCP
super().__init__(**kwargs)
self.start()
def _process_ftp_command(self, payload: FTPPacket) -> FTPPacket:
# handle PORT request
if payload.ftp_command == FTPCommand.PORT:
# check that the port is valid
if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535):
# return successful connection
payload.status_code = FTPStatusCode.OK
# handle STOR request
if payload.ftp_command == FTPCommand.STOR:
# check that the file is created in the computed hosting the FTP server
if self._process_store_data(payload=payload):
payload.status_code = FTPStatusCode.OK
return payload
def _process_store_data(self, payload: FTPPacket) -> bool:
"""Handle the transfer of data from Client to this Server."""
try:
file_name = payload.ftp_command_args["dest_file_name"]
folder_name = payload.ftp_command_args["dest_folder_name"]
file_size = payload.ftp_command_args["file_size"]
self.file_system.create_file(
file_name=file_name,
folder_name=folder_name,
size=file_size,
)
self.sys_log.info(
f"Created item in {self.name}: {payload.ftp_command_args['dest_folder_name']}/"
f"{payload.ftp_command_args['dest_file_name']}"
)
# file should exist
return self.file_system.get_file(file_name=file_name, folder_name=folder_name) is not None
except Exception as e:
self.sys_log.error(f"Unable to store file in {self.name}: {e}")
return False
def receive(self, payload: Any, session_id: Optional[str] = None, **kwargs) -> bool:
"""Receives a payload from the SessionManager."""
if not isinstance(payload, FTPPacket):
self.sys_log.error(f"{payload} is not an FTP packet")
return False
self.send(self._process_ftp_command(payload=payload), session_id)
return True