From 2520b67889cdfcc2d01d590005203c492c5f1344 Mon Sep 17 00:00:00 2001 From: "Czar.Echavez" Date: Mon, 25 Sep 2023 14:31:57 +0100 Subject: [PATCH] #1916: - Added FTP to changelog - Added FTP to documentation - Added documentation in code - Clean up of methods - prevent repeats of the same code --- CHANGELOG.md | 3 +- .../system/ftp_client_server.rst | 62 ++++++++ .../simulation_components/system/software.rst | 1 + .../system/services/ftp/ftp_client.py | 148 ++++++++++++------ .../system/services/ftp/ftp_server.py | 14 +- .../system/services/ftp/ftp_service.py | 51 ++++-- 6 files changed, 215 insertions(+), 64 deletions(-) create mode 100644 docs/source/simulation_components/system/ftp_client_server.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index d9700f83..7147f82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,8 @@ SessionManager. 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) - Red Agent Services: - Data Manipulator Bot - A red agent service which sends a payload to a target machine. (By default this payload is a SQL query that breaks a database) -- DNS Services: DNS Client and DNS Server +- DNS Services: `DNSClient` and `DNSServer` +- FTP Services: `FTPClient` and `FTPServer` ## [2.0.0] - 2023-07-26 diff --git a/docs/source/simulation_components/system/ftp_client_server.rst b/docs/source/simulation_components/system/ftp_client_server.rst new file mode 100644 index 00000000..084d4a85 --- /dev/null +++ b/docs/source/simulation_components/system/ftp_client_server.rst @@ -0,0 +1,62 @@ +.. only:: comment + + © Crown-owned copyright 2023, Defence Science and Technology Laboratory UK + +FTP Client Server +================= + +FTP Server +---------- +Provides a FTP Client-Server simulation by extending the base Service class. + +Key capabilities +^^^^^^^^^^^^^^^^ + +- Simulates FTP requests and FTPPacket transfer across a network +- Allows the emulation of FTP commands between an FTP client and server: + - STOR: stores a file from client to server + - RETR: retrieves a file from the FTP server +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ +- Install on a Node via the ``SoftwareManager`` to start the FTP server service. +- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) + +Implementation +^^^^^^^^^^^^^^ + +- FTP request and responses use a ``FTPPacket`` object +- Extends Service class for integration with ``SoftwareManager``. + +FTP Client +---------- + +The ``FTPClient`` provides a client interface for connecting to the ``FTPServer``. + +Key features +^^^^^^^^^^^^ + +- Connects to the ``FTPServer`` via the ``SoftwareManager``. +- Simulates FTP requests and FTPPacket transfer across a network +- Allows the emulation of FTP commands between an FTP client and server: + - PORT: specifies the port that server should connect to on the client (currently only uses ``Port.FTP``) + - STOR: stores a file from client to server + - RETR: retrieves a file from the FTP server + - QUIT: disconnect from server +- Leverages the Service base class for install/uninstall, status tracking, etc. + +Usage +^^^^^ + +- Install on a Node via the ``SoftwareManager`` to start the FTP client service. +- Service runs on FTP (command) port 21 by default. (TODO: look at in depth implementation of FTP PORT command) +- Execute sending a file to the FTP server with ``send_file`` +- Execute retrieving a file from the FTP server with ``request_file`` + +Implementation +^^^^^^^^^^^^^^ + +- Leverages ``SoftwareManager`` for sending payloads over the network. +- Provides easy interface for Nodes to transfer files between each other. +- Extends base Service class. diff --git a/docs/source/simulation_components/system/software.rst b/docs/source/simulation_components/system/software.rst index 275fdaf9..921dfb9e 100644 --- a/docs/source/simulation_components/system/software.rst +++ b/docs/source/simulation_components/system/software.rst @@ -18,3 +18,4 @@ Contents database_client_server data_manipulation_bot dns_client_server + ftp_client_server diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py index 0e8f3dce..33fe32be 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_client.py +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -19,7 +19,7 @@ class FTPClient(FTPServiceABC): """ connected: bool = False - """Keeps track of whether or not the FTP client is connected to an FTP server""" + """Keeps track of whether or not the FTP client is connected to an FTP server.""" def __init__(self, **kwargs): kwargs["name"] = "FTPClient" @@ -29,7 +29,15 @@ class FTPClient(FTPServiceABC): self.start() def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: - # if server is down, return error + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ + # if client service is down, return error if self.operating_state != ServiceOperatingState.RUNNING: payload.status_code = FTPStatusCode.ERROR return payload @@ -38,20 +46,27 @@ class FTPClient(FTPServiceABC): return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) def _connect_to_server( - self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP + self, + dest_ip_address: Optional[IPv4Address] = None, + dest_port: Optional[Port] = Port.FTP, + is_reattempt: Optional[bool] = False, ) -> 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] + :type: dest_ip_address: 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] + :type: dest_port: Optional[Port] + :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False. + :type: is_reattempt: Optional[bool] """ - # normally FTP will choose a random port for the transfer, but using the FTP command port will do for now + # make sure the service is running before attempting + if self.operating_state != ServiceOperatingState.RUNNING: + self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") + return False + # 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, @@ -61,11 +76,30 @@ class FTPClient(FTPServiceABC): 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 + + if payload.status_code == FTPStatusCode.OK: + return True + else: + if is_reattempt: + # reattempt failed + return False + else: + # try again + self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port, is_reattempt=True) def _disconnect_from_server( self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP ) -> bool: + """ + Connects the client from a given FTP server. + + :param: dest_ip_address: IP address of the FTP server the client needs to disconnect from. Optional. + :type: dest_ip_address: Optional[IPv4Address] + :param: dest_port: Port of the FTP server the client needs to disconnect from. Optional. + :type: dest_port: Optional[Port] + :param: is_reattempt: Set to True if attempt to disconnect from FTP Server has been attempted. Default False. + :type: is_reattempt: Optional[bool] + """ # send a disconnect request payload to FTP server payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) software_manager: SoftwareManager = self.software_manager @@ -85,13 +119,32 @@ class FTPClient(FTPServiceABC): 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.""" - # if service is not running, return error - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") - return False + """ + Send a file to a target IP address. + + The function checks if the file exists in the FTP Client host. + The STOR command is then sent to the FTP Server. + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: IPv4Address + + :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. + :type: src_folder_name: str + + :param: src_file_name: The name of the file to send to the FTP Server. + :type: src_file_name: str + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. + :type: dest_port: Optional[Port] + """ + # check if the file to transfer exists on the client 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}") @@ -104,18 +157,7 @@ class FTPClient(FTPServiceABC): ) 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, - ) + return False else: # send STOR request self._send_data( @@ -137,13 +179,30 @@ class FTPClient(FTPServiceABC): 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.""" - # if service is not running, return error - if self.operating_state != ServiceOperatingState.RUNNING: - self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") - return False + """ + Request a file from a target IP address. + + Sends a RETR command to the FTP Server. + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: IPv4Address + + :param: src_folder_name: The name of the folder that contains the file to send to the FTP Server. + :type: src_folder_name: str + + :param: src_file_name: The name of the file to send to the FTP Server. + :type: src_file_name: str + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. + :type: dest_port: Optional[Port] + """ # check if FTP is currently connected to IP self.connected = self._connect_to_server( dest_ip_address=dest_ip_address, @@ -151,18 +210,7 @@ class FTPClient(FTPServiceABC): ) if not self.connected: - if is_reattempt: - return False - - return self.request_file( - src_folder_name=src_folder_name, - src_file_name=src_file_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, - ) + return False else: # send retrieve request payload: FTPPacket = FTPPacket( @@ -191,7 +239,15 @@ class FTPClient(FTPServiceABC): return False def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: - """Receives a payload from the SessionManager.""" + """ + Receives a payload from the SessionManager. + + :param: payload: FTPPacket payload. + :type: payload: FTPPacket + + :param: session_id: ID of the session. Optional. + :type: session_id: Optional[str] + """ if not isinstance(payload, FTPPacket): self.sys_log.error(f"{payload} is not an FTP packet") return False diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py index 6371d53a..1d028f0b 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_server.py +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -39,23 +39,31 @@ class FTPServer(FTPServiceABC): return self.software_manager.session_manager.sessions_by_uuid[session_id] def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: - # if server is down, return error + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ + # if server service is down, return error if self.operating_state != ServiceOperatingState.RUNNING: payload.status_code = FTPStatusCode.ERROR return payload + session_details = self._get_session_details(session_id) + # process server specific commands, otherwise call super 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 - session_details = self._get_session_details(session_id) self.connections[session_id] = session_details.with_ip_address payload.status_code = FTPStatusCode.OK return payload if payload.ftp_command == FTPCommand.QUIT: - session_details = self._get_session_details(session_id) self.connections.pop(session_id) payload.status_code = FTPStatusCode.OK diff --git a/src/primaite/simulator/system/services/ftp/ftp_service.py b/src/primaite/simulator/system/services/ftp/ftp_service.py index a41d647c..f47b8f64 100644 --- a/src/primaite/simulator/system/services/ftp/ftp_service.py +++ b/src/primaite/simulator/system/services/ftp/ftp_service.py @@ -17,6 +17,14 @@ class FTPServiceABC(Service, ABC): """ def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: + """ + Process the command in the FTP Packet. + + :param: payload: The FTP Packet to process + :type: payload: FTPPacket + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ # handle STOR request if payload.ftp_command == FTPCommand.STOR: # check that the file is created in the computed hosting the FTP server @@ -24,25 +32,14 @@ class FTPServiceABC(Service, ABC): payload.status_code = FTPStatusCode.OK if payload.ftp_command == FTPCommand.RETR: - # check that the file exists in the FTP Server - file: File = self.file_system.get_file( - folder_name=payload.ftp_command_args["src_folder_name"], - file_name=payload.ftp_command_args["src_file_name"], - ) - if file: + if self._retrieve_data(payload=payload, session_id=session_id): payload.status_code = FTPStatusCode.OK - self._send_data( - file=file, - dest_folder_name=payload.ftp_command_args["dest_folder_name"], - dest_file_name=payload.ftp_command_args["dest_file_name"], - session_id=session_id, - ) return payload def _store_data(self, payload: FTPPacket) -> bool: """ - Handle the transfer of data. + Stores the data in the FTP Service's host machine. :param: payload: The FTP Packet that contains the file data :type: FTPPacket @@ -75,6 +72,27 @@ class FTPServiceABC(Service, ABC): dest_port: Optional[Port] = None, session_id: Optional[str] = None, ) -> bool: + """ + Sends data from the host FTP Service's machine to another FTP Service's host machine. + + :param: file: File to send to the target FTP Service. + :type: file: File + + :param: dest_folder_name: The name of the folder where the file will be stored in the FTP Server. + :type: dest_folder_name: str + + :param: dest_file_name: The name of the file to be saved on the FTP Server. + :type: dest_file_name: str + + :param: dest_ip_address: The IP address of the machine that hosts the FTP Server. + :type: dest_ip_address: Optional[IPv4Address] + + :param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP. + :type: dest_port: Optional[Port] + + :param: session_id: session ID linked to the FTP Packet. Optional. + :type: session_id: Optional[str] + """ # send STOR request payload: FTPPacket = FTPPacket( ftp_command=FTPCommand.STOR, @@ -106,6 +124,8 @@ class FTPServiceABC(Service, ABC): # find the file file_name = payload.ftp_command_args["src_file_name"] folder_name = payload.ftp_command_args["src_folder_name"] + dest_folder_name = payload.ftp_command_args["dest_folder_name"] + dest_file_name = payload.ftp_command_args["dest_file_name"] retrieved_file: File = self.file_system.get_file(folder_name=folder_name, file_name=file_name) # if file does not exist, return an error @@ -118,7 +138,10 @@ class FTPServiceABC(Service, ABC): else: # send requested data return self._send_data( - file=retrieved_file, dest_file_name=file_name, dest_folder_name=folder_name, session_id=session_id + file=retrieved_file, + dest_file_name=dest_file_name, + dest_folder_name=dest_folder_name, + session_id=session_id, ) except Exception as e: self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}")