2025-01-02 15:05:06 +00:00
|
|
|
# © Crown-owned copyright 2025, Defence Science and Technology Laboratory UK
|
2025-03-13 15:07:32 +00:00
|
|
|
"""FTP Server."""
|
2023-12-08 17:07:57 +00:00
|
|
|
from typing import Any, Optional
|
2023-09-20 16:23:35 +01:00
|
|
|
|
2025-01-03 13:39:58 +00:00
|
|
|
from pydantic import Field
|
|
|
|
|
|
2023-11-29 13:18:38 +00:00
|
|
|
from primaite import getLogger
|
2023-09-22 15:38:01 +01:00
|
|
|
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
2023-09-21 15:13:30 +01:00
|
|
|
from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC
|
2024-09-25 16:28:22 +01:00
|
|
|
from primaite.utils.validation.ip_protocol import PROTOCOL_LOOKUP
|
|
|
|
|
from primaite.utils.validation.port import is_valid_port, PORT_LOOKUP
|
2023-09-20 16:23:35 +01:00
|
|
|
|
2023-11-29 13:18:38 +00:00
|
|
|
_LOGGER = getLogger(__name__)
|
|
|
|
|
|
2023-09-20 16:23:35 +01:00
|
|
|
|
2025-02-03 16:24:03 +00:00
|
|
|
class FTPServer(FTPServiceABC, discriminator="ftp-server"):
|
2023-09-20 16:23:35 +01:00
|
|
|
"""
|
|
|
|
|
A class for simulating an FTP server service.
|
|
|
|
|
|
2024-12-10 16:58:28 +00:00
|
|
|
This class inherits from the `FTPServiceABC` class and provides methods to emulate FTP
|
2023-09-20 16:23:35 +01:00
|
|
|
RFC 959: https://datatracker.ietf.org/doc/html/rfc959
|
|
|
|
|
"""
|
|
|
|
|
|
2025-01-31 12:18:52 +00:00
|
|
|
class ConfigSchema(FTPServiceABC.ConfigSchema):
|
2024-12-10 16:58:28 +00:00
|
|
|
"""ConfigSchema for FTPServer."""
|
|
|
|
|
|
2025-02-03 16:24:03 +00:00
|
|
|
type: str = "ftp-server"
|
2025-01-31 12:18:52 +00:00
|
|
|
server_password: Optional[str] = None
|
2024-12-10 16:58:28 +00:00
|
|
|
|
2025-02-03 16:24:03 +00:00
|
|
|
config: ConfigSchema = Field(default_factory=lambda: FTPServer.ConfigSchema())
|
2025-02-04 15:20:48 +00:00
|
|
|
server_password: Optional[str] = None
|
2025-02-03 16:24:03 +00:00
|
|
|
|
2023-09-20 16:23:35 +01:00
|
|
|
def __init__(self, **kwargs):
|
2025-02-03 16:24:03 +00:00
|
|
|
kwargs["name"] = "ftp-server"
|
2024-09-20 11:21:28 +01:00
|
|
|
kwargs["port"] = PORT_LOOKUP["FTP"]
|
|
|
|
|
kwargs["protocol"] = PROTOCOL_LOOKUP["TCP"]
|
2023-09-20 16:23:35 +01:00
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
self.start()
|
|
|
|
|
|
2025-02-03 11:16:34 +00:00
|
|
|
@property
|
|
|
|
|
def server_password(self) -> Optional[str]:
|
|
|
|
|
"""Convenience method for accessing FTP server password."""
|
|
|
|
|
return self.config.server_password
|
2023-09-20 16:23:35 +01:00
|
|
|
|
2023-09-22 15:38:01 +01:00
|
|
|
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket:
|
2023-09-25 14:31:57 +01:00
|
|
|
"""
|
|
|
|
|
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]
|
|
|
|
|
"""
|
2023-10-05 16:24:48 +01:00
|
|
|
# error code by default
|
|
|
|
|
payload.status_code = FTPStatusCode.ERROR
|
|
|
|
|
|
2023-09-25 14:31:57 +01:00
|
|
|
# if server service is down, return error
|
2023-11-29 01:28:40 +00:00
|
|
|
if not self._can_perform_action():
|
2023-09-22 15:38:01 +01:00
|
|
|
return payload
|
|
|
|
|
|
2023-10-03 14:59:48 +01:00
|
|
|
self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}")
|
|
|
|
|
|
2023-09-28 12:23:49 +01:00
|
|
|
if payload.ftp_command is not None:
|
|
|
|
|
self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.")
|
|
|
|
|
|
2023-09-22 15:38:01 +01:00
|
|
|
# process server specific commands, otherwise call super
|
|
|
|
|
if payload.ftp_command == FTPCommand.PORT:
|
|
|
|
|
# check that the port is valid
|
2024-09-25 16:28:22 +01:00
|
|
|
if is_valid_port(payload.ftp_command_args):
|
2023-09-22 15:38:01 +01:00
|
|
|
# return successful connection
|
2023-12-08 17:07:57 +00:00
|
|
|
self.add_connection(connection_id=session_id, session_id=session_id)
|
2023-09-22 15:38:01 +01:00
|
|
|
payload.status_code = FTPStatusCode.OK
|
|
|
|
|
return payload
|
|
|
|
|
|
2023-10-05 16:24:48 +01:00
|
|
|
self.sys_log.error(f"Invalid Port {payload.ftp_command_args}")
|
|
|
|
|
return payload
|
|
|
|
|
|
2023-09-22 15:38:01 +01:00
|
|
|
if payload.ftp_command == FTPCommand.QUIT:
|
2024-04-26 14:52:21 +00:00
|
|
|
self.terminate_connection(connection_id=session_id)
|
2023-09-22 15:38:01 +01:00
|
|
|
payload.status_code = FTPStatusCode.OK
|
2023-10-05 16:24:48 +01:00
|
|
|
return payload
|
2023-09-22 15:38:01 +01:00
|
|
|
|
|
|
|
|
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
|
|
|
|
|
|
2023-09-20 16:23:35 +01:00
|
|
|
def receive(self, payload: Any, session_id: Optional[str] = None, **kwargs) -> bool:
|
|
|
|
|
"""Receives a payload from the SessionManager."""
|
|
|
|
|
if not isinstance(payload, FTPPacket):
|
2024-04-19 11:37:52 +01:00
|
|
|
self.sys_log.warning(f"{self.name}: Payload is not an FTP packet")
|
|
|
|
|
self.sys_log.debug(f"{self.name}: {payload}")
|
2023-09-20 16:23:35 +01:00
|
|
|
return False
|
|
|
|
|
|
2023-11-29 01:28:40 +00:00
|
|
|
if not super().receive(payload=payload, session_id=session_id, **kwargs):
|
|
|
|
|
return False
|
|
|
|
|
|
2023-10-05 16:24:48 +01:00
|
|
|
"""
|
2023-10-09 13:25:12 +01:00
|
|
|
Ignore ftp payload if status code is defined.
|
|
|
|
|
|
|
|
|
|
This means that an FTP server has already handled the packet and
|
|
|
|
|
prevents an FTP request loop - FTP client and servers can exist on
|
|
|
|
|
the same node.
|
2023-10-05 16:24:48 +01:00
|
|
|
"""
|
2023-11-23 21:48:11 +00:00
|
|
|
if payload.status_code is not None:
|
2023-11-23 19:49:03 +00:00
|
|
|
return False
|
|
|
|
|
|
2024-01-10 18:04:48 +00:00
|
|
|
self._process_ftp_command(payload=payload, session_id=session_id)
|
2023-09-20 16:23:35 +01:00
|
|
|
return True
|