- 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)
- 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

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
data_manipulation_bot
dns_client_server
ftp_client_server

View File

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

View File

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

View File

@@ -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}")