- Added FTP to changelog
- Added FTP to documentation
- Added documentation in code
- Clean up of methods - prevent repeats of the same code
This commit is contained in:
Czar.Echavez
2023-09-25 14:31:57 +01:00
parent 98ca71aaba
commit 2520b67889
6 changed files with 215 additions and 64 deletions

View File

@@ -29,7 +29,8 @@ SessionManager.
1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP) 1. Creating a simulation - this notebook explains how to build up a simulation using the Python package. (WIP)
- Red Agent Services: - 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) - 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 ## [2.0.0] - 2023-07-26

View File

@@ -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.

View File

@@ -18,3 +18,4 @@ Contents
database_client_server database_client_server
data_manipulation_bot data_manipulation_bot
dns_client_server dns_client_server
ftp_client_server

View File

@@ -19,7 +19,7 @@ class FTPClient(FTPServiceABC):
""" """
connected: bool = False 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): def __init__(self, **kwargs):
kwargs["name"] = "FTPClient" kwargs["name"] = "FTPClient"
@@ -29,7 +29,15 @@ class FTPClient(FTPServiceABC):
self.start() self.start()
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: 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: if self.operating_state != ServiceOperatingState.RUNNING:
payload.status_code = FTPStatusCode.ERROR payload.status_code = FTPStatusCode.ERROR
return payload return payload
@@ -38,20 +46,27 @@ class FTPClient(FTPServiceABC):
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs) return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
def _connect_to_server( 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: ) -> bool:
""" """
Connects the client to a given FTP server. 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. :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. :param: dest_port: Port of the FTP server the client needs to connect to. Optional.
:type: Optional[Port] :type: dest_port: Optional[Port]
:param: server_password: The password to use when connecting to the FTP server. Optional. :param: is_reattempt: Set to True if attempt to connect to FTP Server has been attempted. Default False.
:type: Optional[str] :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 # create FTP packet
payload: FTPPacket = FTPPacket( payload: FTPPacket = FTPPacket(
ftp_command=FTPCommand.PORT, ftp_command=FTPCommand.PORT,
@@ -61,11 +76,30 @@ class FTPClient(FTPServiceABC):
software_manager.send_payload_to_session_manager( software_manager.send_payload_to_session_manager(
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port 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( def _disconnect_from_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
) -> bool: ) -> 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 # send a disconnect request payload to FTP server
payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT) payload: FTPPacket = FTPPacket(ftp_command=FTPCommand.QUIT)
software_manager: SoftwareManager = self.software_manager software_manager: SoftwareManager = self.software_manager
@@ -85,13 +119,32 @@ class FTPClient(FTPServiceABC):
dest_folder_name: str, dest_folder_name: str,
dest_file_name: str, dest_file_name: str,
dest_port: Optional[Port] = Port.FTP, dest_port: Optional[Port] = Port.FTP,
is_reattempt: Optional[bool] = False,
) -> bool: ) -> bool:
"""Send a file to a target IP address.""" """
# if service is not running, return error Send a file to a target IP address.
if self.operating_state != ServiceOperatingState.RUNNING:
self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") The function checks if the file exists in the FTP Client host.
return False 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) file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name)
if not file_to_transfer: if not file_to_transfer:
self.sys_log.error(f"Unable to send file that does not exist: {src_folder_name}/{src_file_name}") 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 not self.connected:
if is_reattempt:
return False 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: else:
# send STOR request # send STOR request
self._send_data( self._send_data(
@@ -137,13 +179,30 @@ class FTPClient(FTPServiceABC):
dest_folder_name: str, dest_folder_name: str,
dest_file_name: str, dest_file_name: str,
dest_port: Optional[Port] = Port.FTP, dest_port: Optional[Port] = Port.FTP,
is_reattempt: Optional[bool] = False,
) -> bool: ) -> bool:
"""Request a file from a target IP address.""" """
# if service is not running, return error Request a file from a target IP address.
if self.operating_state != ServiceOperatingState.RUNNING:
self.sys_log.error(f"FTPClient not running for {self.sys_log.hostname}") Sends a RETR command to the FTP Server.
return False
: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 # check if FTP is currently connected to IP
self.connected = self._connect_to_server( self.connected = self._connect_to_server(
dest_ip_address=dest_ip_address, dest_ip_address=dest_ip_address,
@@ -151,18 +210,7 @@ class FTPClient(FTPServiceABC):
) )
if not self.connected: if not self.connected:
if is_reattempt:
return False 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,
)
else: else:
# send retrieve request # send retrieve request
payload: FTPPacket = FTPPacket( payload: FTPPacket = FTPPacket(
@@ -191,7 +239,15 @@ class FTPClient(FTPServiceABC):
return False return False
def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: 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): if not isinstance(payload, FTPPacket):
self.sys_log.error(f"{payload} is not an FTP packet") self.sys_log.error(f"{payload} is not an FTP packet")
return False return False

View File

@@ -39,23 +39,31 @@ class FTPServer(FTPServiceABC):
return self.software_manager.session_manager.sessions_by_uuid[session_id] 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: 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: if self.operating_state != ServiceOperatingState.RUNNING:
payload.status_code = FTPStatusCode.ERROR payload.status_code = FTPStatusCode.ERROR
return payload return payload
session_details = self._get_session_details(session_id)
# process server specific commands, otherwise call super # process server specific commands, otherwise call super
if payload.ftp_command == FTPCommand.PORT: if payload.ftp_command == FTPCommand.PORT:
# check that the port is valid # check that the port is valid
if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535): if isinstance(payload.ftp_command_args, Port) and payload.ftp_command_args.value in range(0, 65535):
# return successful connection # return successful connection
session_details = self._get_session_details(session_id)
self.connections[session_id] = session_details.with_ip_address self.connections[session_id] = session_details.with_ip_address
payload.status_code = FTPStatusCode.OK payload.status_code = FTPStatusCode.OK
return payload return payload
if payload.ftp_command == FTPCommand.QUIT: if payload.ftp_command == FTPCommand.QUIT:
session_details = self._get_session_details(session_id)
self.connections.pop(session_id) self.connections.pop(session_id)
payload.status_code = FTPStatusCode.OK payload.status_code = FTPStatusCode.OK

View File

@@ -17,6 +17,14 @@ class FTPServiceABC(Service, ABC):
""" """
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket: 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 # handle STOR request
if payload.ftp_command == FTPCommand.STOR: if payload.ftp_command == FTPCommand.STOR:
# check that the file is created in the computed hosting the FTP server # 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 payload.status_code = FTPStatusCode.OK
if payload.ftp_command == FTPCommand.RETR: if payload.ftp_command == FTPCommand.RETR:
# check that the file exists in the FTP Server if self._retrieve_data(payload=payload, session_id=session_id):
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:
payload.status_code = FTPStatusCode.OK 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 return payload
def _store_data(self, payload: FTPPacket) -> bool: 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 :param: payload: The FTP Packet that contains the file data
:type: FTPPacket :type: FTPPacket
@@ -75,6 +72,27 @@ class FTPServiceABC(Service, ABC):
dest_port: Optional[Port] = None, dest_port: Optional[Port] = None,
session_id: Optional[str] = None, session_id: Optional[str] = None,
) -> bool: ) -> 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 # send STOR request
payload: FTPPacket = FTPPacket( payload: FTPPacket = FTPPacket(
ftp_command=FTPCommand.STOR, ftp_command=FTPCommand.STOR,
@@ -106,6 +124,8 @@ class FTPServiceABC(Service, ABC):
# find the file # find the file
file_name = payload.ftp_command_args["src_file_name"] file_name = payload.ftp_command_args["src_file_name"]
folder_name = payload.ftp_command_args["src_folder_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) retrieved_file: File = self.file_system.get_file(folder_name=folder_name, file_name=file_name)
# if file does not exist, return an error # if file does not exist, return an error
@@ -118,7 +138,10 @@ class FTPServiceABC(Service, ABC):
else: else:
# send requested data # send requested data
return self._send_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: except Exception as e:
self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}") self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}")