diff --git a/src/primaite/simulator/network/networks.py b/src/primaite/simulator/network/networks.py index f594c29a..63cb05e0 100644 --- a/src/primaite/simulator/network/networks.py +++ b/src/primaite/simulator/network/networks.py @@ -12,6 +12,8 @@ from primaite.simulator.system.applications.database_client import DatabaseClien from primaite.simulator.system.services.database.database_service import DatabaseService from primaite.simulator.system.services.dns.dns_client import DNSClient from primaite.simulator.system.services.dns.dns_server import DNSServer +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot @@ -136,13 +138,13 @@ def arcd_uc2_network() -> Network: ) client_1.power_on() client_1.software_manager.install(DNSClient) - client_1_dns_client_service: DNSServer = client_1.software_manager.software["DNSClient"] # noqa - client_1_dns_client_service.start() network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1]) client_1.software_manager.install(DataManipulationBot) db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"] db_manipulation_bot.configure(server_ip_address=IPv4Address("192.168.1.14"), payload="DROP TABLE IF EXISTS user;") + client_1.software_manager.install(FTPClient) + # Client 2 client_2 = Computer( hostname="client_2", @@ -153,10 +155,10 @@ def arcd_uc2_network() -> Network: ) client_2.power_on() client_2.software_manager.install(DNSClient) - client_2_dns_client_service: DNSServer = client_2.software_manager.software["DNSClient"] # noqa - client_2_dns_client_service.start() network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2]) + client_2.software_manager.install(FTPClient) + # Domain Controller domain_controller = Server( hostname="domain_controller", @@ -191,20 +193,48 @@ def arcd_uc2_network() -> Network: );""" user_insert_statements = [ - "INSERT INTO user (name, email, age, city, occupation) VALUES ('John Doe', 'johndoe@example.com', 32, 'New York', 'Engineer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Alice Lee', 'alicelee@example.com', 22, 'San Francisco', 'Student');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('David Kim', 'davidkim@example.com', 38, 'Houston', 'Consultant');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Emily Chen', 'emilychen@example.com', 29, 'Seattle', 'Software Developer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Frank Wang', 'frankwang@example.com', 55, 'New York', 'Entrepreneur');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Grace Park', 'gracepark@example.com', 31, 'Los Angeles', 'Marketing Specialist');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Henry Wu', 'henrywu@example.com', 40, 'Chicago', 'Accountant');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Isabella Kim', 'isabellakim@example.com', 26, 'San Francisco', 'Graphic Designer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Jake Lee', 'jakelee@example.com', 33, 'Houston', 'Sales Manager');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Kelly Chen', 'kellychen@example.com', 28, 'Seattle', 'Web Developer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Lucas Liu', 'lucasliu@example.com', 42, 'New York', 'Lawyer');", # noqa - "INSERT INTO user (name, email, age, city, occupation) VALUES ('Maggie Wang', 'maggiewang@example.com', 30, 'Los Angeles', 'Data Analyst');", # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('John Doe', 'johndoe@example.com', 32, 'New York', 'Engineer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Alice Lee', 'alicelee@example.com', 22, 'San Francisco', 'Student');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('David Kim', 'davidkim@example.com', 38, 'Houston', 'Consultant');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Emily Chen', 'emilychen@example.com', 29, 'Seattle', 'Software Developer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Frank Wang', 'frankwang@example.com', 55, 'New York', 'Entrepreneur');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Grace Park', 'gracepark@example.com', 31, 'Los Angeles', 'Marketing Specialist');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Henry Wu', 'henrywu@example.com', 40, 'Chicago', 'Accountant');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Isabella Kim', 'isabellakim@example.com', 26, 'San Francisco', 'Graphic Designer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Jake Lee', 'jakelee@example.com', 33, 'Houston', 'Sales Manager');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Kelly Chen', 'kellychen@example.com', 28, 'Seattle', 'Web Developer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Lucas Liu', 'lucasliu@example.com', 42, 'New York', 'Lawyer');", + # noqa + "INSERT INTO user (name, email, age, city, occupation) " + "VALUES ('Maggie Wang', 'maggiewang@example.com', 30, 'Los Angeles', 'Data Analyst');", + # noqa ] database_server.software_manager.install(DatabaseService) database_service: DatabaseService = database_server.software_manager.software["DatabaseService"] # noqa @@ -232,7 +262,6 @@ def arcd_uc2_network() -> Network: # register the web_server to a domain dns_server_service: DNSServer = domain_controller.software_manager.software["DNSServer"] # noqa - dns_server_service.start() dns_server_service.dns_register("arcd.com", web_server.ip_address) # Backup Server @@ -246,6 +275,8 @@ def arcd_uc2_network() -> Network: backup_server.power_on() network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4]) + backup_server.software_manager.install(FTPServer) + # Security Suite security_suite = Server( hostname="security_suite", @@ -271,4 +302,7 @@ def arcd_uc2_network() -> Network: # Allow DNS requests router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1) + # Allow FTP requests + router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2) + return network diff --git a/src/primaite/simulator/network/protocols/arp.py b/src/primaite/simulator/network/protocols/arp.py index 5e38cc66..7b3e4509 100644 --- a/src/primaite/simulator/network/protocols/arp.py +++ b/src/primaite/simulator/network/protocols/arp.py @@ -5,6 +5,8 @@ from typing import Optional from pydantic import BaseModel +from primaite.simulator.network.protocols.packet import DataPacket + class ARPEntry(BaseModel): """ @@ -18,7 +20,7 @@ class ARPEntry(BaseModel): nic_uuid: str -class ARPPacket(BaseModel): +class ARPPacket(DataPacket): """ Represents the ARP layer of a network frame. diff --git a/src/primaite/simulator/network/protocols/dns.py b/src/primaite/simulator/network/protocols/dns.py index 41bf5e0c..4f9be51b 100644 --- a/src/primaite/simulator/network/protocols/dns.py +++ b/src/primaite/simulator/network/protocols/dns.py @@ -5,6 +5,8 @@ from typing import Optional from pydantic import BaseModel +from primaite.simulator.network.protocols.packet import DataPacket + class DNSRequest(BaseModel): """Represents a DNS Request packet of a network frame. @@ -26,7 +28,7 @@ class DNSReply(BaseModel): "IP Address of the Domain Name requested." -class DNSPacket(BaseModel): +class DNSPacket(DataPacket): """ Represents the DNS layer of a network frame. diff --git a/src/primaite/simulator/network/protocols/ftp.py b/src/primaite/simulator/network/protocols/ftp.py new file mode 100644 index 00000000..ab277045 --- /dev/null +++ b/src/primaite/simulator/network/protocols/ftp.py @@ -0,0 +1,55 @@ +from enum import Enum +from typing import Any + +from primaite.simulator.network.protocols.packet import DataPacket + + +class FTPCommand(Enum): + """FTP Commands that are allowed.""" + + PORT = "PORT" + """Set a port to be used for the FTP transfer.""" + + STOR = "STOR" + """Copy or put data to the FTP server.""" + + RETR = "RETR" + """Retrieve data from the FTP server.""" + + DELE = "DELE" + """Delete the file in the specified path.""" + + RMD = "RMD" + """Remove the directory in the specified path.""" + + MKD = "MKD" + """Make a directory in the specified path.""" + + LIST = "LIST" + """Return a list of files in the specified path.""" + + QUIT = "QUIT" + """Ends connection between client and server.""" + + +class FTPStatusCode(Enum): + """Status code of the current FTP request.""" + + OK = 200 + """Command successful.""" + + ERROR = 500 + """General error code.""" + + +class FTPPacket(DataPacket): + """Represents an FTP Packet.""" + + ftp_command: FTPCommand + """Command type of the packet.""" + + ftp_command_args: Any + """Arguments for command.""" + + status_code: FTPStatusCode = None + """Status of the response.""" diff --git a/src/primaite/simulator/network/protocols/packet.py b/src/primaite/simulator/network/protocols/packet.py new file mode 100644 index 00000000..1adcc800 --- /dev/null +++ b/src/primaite/simulator/network/protocols/packet.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class DataPacket(BaseModel): + """Data packet abstract class.""" + + packet_payload_size: float = 0 + """Size of the packet.""" + + def get_packet_size(self) -> float: + """Returns the size of the packet header and payload.""" + return self.packet_payload_size + float(len(self.model_dump_json().encode("utf-8"))) diff --git a/src/primaite/simulator/network/transmission/data_link_layer.py b/src/primaite/simulator/network/transmission/data_link_layer.py index b7986622..fa823a60 100644 --- a/src/primaite/simulator/network/transmission/data_link_layer.py +++ b/src/primaite/simulator/network/transmission/data_link_layer.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from primaite import getLogger from primaite.simulator.network.protocols.arp import ARPPacket +from primaite.simulator.network.protocols.packet import DataPacket from primaite.simulator.network.transmission.network_layer import ICMPPacket, IPPacket, IPProtocol from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader @@ -132,6 +133,10 @@ class Frame(BaseModel): @property def size(self) -> float: # noqa - Keep it as MBits as this is how they're expressed """The size of the Frame in Bytes.""" + # get the payload size if it is a data packet + if isinstance(self.payload, DataPacket): + return self.payload.get_packet_size() + return float(len(self.model_dump_json().encode("utf-8"))) @property diff --git a/src/primaite/simulator/system/services/dns/dns_client.py b/src/primaite/simulator/system/services/dns/dns_client.py index cf5278af..56d5d8b4 100644 --- a/src/primaite/simulator/system/services/dns/dns_client.py +++ b/src/primaite/simulator/system/services/dns/dns_client.py @@ -27,6 +27,7 @@ class DNSClient(Service): # TCP for now kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) + self.start() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/dns/dns_server.py b/src/primaite/simulator/system/services/dns/dns_server.py index c6a9afd3..c3c39595 100644 --- a/src/primaite/simulator/system/services/dns/dns_server.py +++ b/src/primaite/simulator/system/services/dns/dns_server.py @@ -26,6 +26,7 @@ class DNSServer(Service): # TCP for now kwargs["protocol"] = IPProtocol.TCP super().__init__(**kwargs) + self.start() def describe_state(self) -> Dict: """ diff --git a/src/primaite/simulator/system/services/ftp/__init__.py b/src/primaite/simulator/system/services/ftp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/primaite/simulator/system/services/ftp/ftp_client.py b/src/primaite/simulator/system/services/ftp/ftp_client.py new file mode 100644 index 00000000..687e9a12 --- /dev/null +++ b/src/primaite/simulator/system/services/ftp/ftp_client.py @@ -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 diff --git a/src/primaite/simulator/system/services/ftp/ftp_server.py b/src/primaite/simulator/system/services/ftp/ftp_server.py new file mode 100644 index 00000000..ead6d503 --- /dev/null +++ b/src/primaite/simulator/system/services/ftp/ftp_server.py @@ -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 diff --git a/tests/integration_tests/system/test_ftp_client_server.py b/tests/integration_tests/system/test_ftp_client_server.py new file mode 100644 index 00000000..e062e0b7 --- /dev/null +++ b/tests/integration_tests/system/test_ftp_client_server.py @@ -0,0 +1,59 @@ +from ipaddress import IPv4Address + +from primaite.simulator.network.hardware.nodes.computer import Computer +from primaite.simulator.network.hardware.nodes.server import Server +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer +from primaite.simulator.system.services.service import ServiceOperatingState + + +def test_ftp_client_store_file_in_server(uc2_network): + """ + Test checks to see if the client is able to store files in the backup server. + """ + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + backup_server: Server = uc2_network.get_node_by_hostname("backup_server") + + ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] + ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp client + ftp_client.file_system.create_file(file_name="test_file.txt") + + ftp_client.send_file( + src_folder_name="root", + src_file_name="test_file.txt", + dest_folder_name="client_1_backup", + dest_file_name="test_file.txt", + dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + ) + + assert ftp_server.file_system.get_file(folder_name="client_1_backup", file_name="test_file.txt") + + +def test_ftp_client_retrieve_file_from_server(uc2_network): + """ + Test checks to see if the client is able to retrieve files from the backup server. + """ + client_1: Computer = uc2_network.get_node_by_hostname("client_1") + backup_server: Server = uc2_network.get_node_by_hostname("backup_server") + + ftp_client: FTPClient = client_1.software_manager.software["FTPClient"] + ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"] + + assert ftp_client.operating_state == ServiceOperatingState.RUNNING + assert ftp_server.operating_state == ServiceOperatingState.RUNNING + + # create file on ftp server + ftp_server.file_system.create_file(file_name="test_file.txt", folder_name="file_share") + + ftp_client.request_file( + src_folder_name="file_share", + src_file_name="test_file.txt", + dest_folder_name="downloads", + dest_file_name="test_file.txt", + dest_ip_address=backup_server.nics.get(next(iter(backup_server.nics))).ip_address, + ) diff --git a/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py new file mode 100644 index 00000000..64013207 --- /dev/null +++ b/tests/unit_tests/_primaite/_simulator/_system/_services/test_ftp.py @@ -0,0 +1,41 @@ +from ipaddress import IPv4Address + +import pytest + +from primaite.simulator.network.hardware.base import Node +from primaite.simulator.network.transmission.network_layer import IPProtocol +from primaite.simulator.network.transmission.transport_layer import Port +from primaite.simulator.system.services.ftp.ftp_client import FTPClient +from primaite.simulator.system.services.ftp.ftp_server import FTPServer + + +@pytest.fixture(scope="function") +def ftp_server() -> Node: + node = Node(hostname="ftp_server") + node.software_manager.install(software_class=FTPServer) + node.software_manager.software["FTPServer"].start() + return node + + +@pytest.fixture(scope="function") +def ftp_client() -> Node: + node = Node(hostname="ftp_client") + node.software_manager.install(software_class=FTPClient) + node.software_manager.software["FTPClient"].start() + return node + + +def test_create_ftp_server(ftp_server): + assert ftp_server is not None + ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"] + assert ftp_server_service.name is "FTPServer" + assert ftp_server_service.port is Port.FTP + assert ftp_server_service.protocol is IPProtocol.TCP + + +def test_create_ftp_client(ftp_client): + assert ftp_client is not None + ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"] + assert ftp_client_service.name is "FTPClient" + assert ftp_client_service.port is Port.FTP + assert ftp_client_service.protocol is IPProtocol.TCP