Merge remote-tracking branch 'origin/dev' into feature/1812-traverse-actions-dict
This commit is contained in:
@@ -5,10 +5,12 @@
|
|||||||
*How have you tested this (if applicable)?*
|
*How have you tested this (if applicable)?*
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
- [ ] This PR is linked to a **work item**
|
- [ ] PR is linked to a **work item**
|
||||||
- [ ] I have performed **self-review** of the code
|
- [ ] **acceptance criteria** of linked ticket are met
|
||||||
- [ ] I have written **tests** for any new functionality added with this PR
|
- [ ] performed **self-review** of the code
|
||||||
- [ ] I have updated the **documentation** if this PR changes or adds functionality
|
- [ ] written **tests** for any new functionality added with this PR
|
||||||
- [ ] I have written/updated **design docs** if this PR implements new functionality
|
- [ ] updated the **documentation** if this PR changes or adds functionality
|
||||||
- [ ] I have update the **change log**
|
- [ ] written/updated **design docs** if this PR implements new functionality
|
||||||
- [ ] I have run **pre-commit** checks for code style
|
- [ ] updated the **change log**
|
||||||
|
- [ ] ran **pre-commit** checks for code style
|
||||||
|
- [ ] attended to any **TO-DOs** left in the code
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ SessionManager.
|
|||||||
- File System - ability to emulate a node's file system during a simulation
|
- File System - ability to emulate a node's file system during a simulation
|
||||||
- Example notebooks - There is currently 1 jupyter notebook which walks through using PrimAITE
|
- Example notebooks - There is currently 1 jupyter notebook which walks through using PrimAITE
|
||||||
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)
|
||||||
|
- Database:
|
||||||
|
- `DatabaseClient` and `DatabaseService` created to allow emulation of database actions
|
||||||
|
- Ability for `DatabaseService` to backup its data to another server via FTP and restore data from backup
|
||||||
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ Usage
|
|||||||
- Retrieve results in a dictionary.
|
- Retrieve results in a dictionary.
|
||||||
- Disconnect when finished.
|
- Disconnect when finished.
|
||||||
|
|
||||||
|
To create database backups:
|
||||||
|
|
||||||
|
- Configure the backup server on the ``DatabaseService`` by providing the Backup server ``IPv4Address`` with ``configure_backup``
|
||||||
|
- Create a backup using ``backup_database``. This fails if the backup server is not configured.
|
||||||
|
- Restore a backup using ``restore_backup``. By default, this uses the database created via ``backup_database``.
|
||||||
|
|
||||||
Implementation
|
Implementation
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|||||||
126
docs/source/simulation_components/system/ftp_client_server.rst
Normal file
126
docs/source/simulation_components/system/ftp_client_server.rst
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
.. 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.
|
||||||
|
|
||||||
|
|
||||||
|
Example Usage
|
||||||
|
----------
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
from primaite.simulator.network.container import Network
|
||||||
|
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_server import FTPServer
|
||||||
|
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||||
|
|
||||||
|
Example peer to peer network
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
net = Network()
|
||||||
|
|
||||||
|
pc1 = Computer(hostname="pc1", ip_address="120.10.10.10", subnet_mask="255.255.255.0")
|
||||||
|
srv = Server(hostname="srv", ip_address="120.10.10.20", subnet_mask="255.255.255.0")
|
||||||
|
pc1.power_on()
|
||||||
|
srv.power_on()
|
||||||
|
net.connect(pc1.ethernet_port[1], srv.ethernet_port[1])
|
||||||
|
|
||||||
|
Install the FTP Server
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
FTP Client should be pre installed on nodes
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
srv.software_manager.install(FTPServer)
|
||||||
|
ftpserv: FTPServer = srv.software_manager.software['FTPServer']
|
||||||
|
|
||||||
|
Setting up the FTP Server
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Set up the FTP Server with a file that the client will need to retrieve
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
srv.file_system.create_file('my_file.png')
|
||||||
|
|
||||||
|
Check that file was retrieved
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
client.request_file(
|
||||||
|
src_folder_name='root',
|
||||||
|
src_file_name='my_file.png',
|
||||||
|
dest_folder_name='root',
|
||||||
|
dest_file_name='test.png',
|
||||||
|
dest_ip_address=IPv4Address("120.10.10.20")
|
||||||
|
)
|
||||||
|
|
||||||
|
print(client.get_file(folder_name="root", file_name="test.png"))
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ class FileSystem(SimComponent):
|
|||||||
folder = self.get_folder(folder_name)
|
folder = self.get_folder(folder_name)
|
||||||
if folder:
|
if folder:
|
||||||
return folder.get_file(file_name)
|
return folder.get_file(file_name)
|
||||||
self.fs.sys_log.info(f"file not found /{folder_name}/{file_name}")
|
self.sys_log.info(f"file not found /{folder_name}/{file_name}")
|
||||||
|
|
||||||
def delete_file(self, folder_name: str, file_name: str):
|
def delete_file(self, folder_name: str, file_name: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -502,7 +502,9 @@ class Link(SimComponent):
|
|||||||
def _can_transmit(self, frame: Frame) -> bool:
|
def _can_transmit(self, frame: Frame) -> bool:
|
||||||
if self.is_up:
|
if self.is_up:
|
||||||
frame_size_Mbits = frame.size_Mbits # noqa - Leaving it as Mbits as this is how they're expressed
|
frame_size_Mbits = frame.size_Mbits # noqa - Leaving it as Mbits as this is how they're expressed
|
||||||
return self.current_load + frame_size_Mbits <= self.bandwidth
|
# return self.current_load + frame_size_Mbits <= self.bandwidth
|
||||||
|
# TODO: re add this check once packet size limiting and MTU checks are implemented
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def transmit_frame(self, sender_nic: Union[NIC, SwitchPort], frame: Frame) -> bool:
|
def transmit_frame(self, sender_nic: Union[NIC, SwitchPort], frame: Frame) -> bool:
|
||||||
@@ -942,6 +944,7 @@ class Node(SimComponent):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.arp.nics = self.nics
|
self.arp.nics = self.nics
|
||||||
self.session_manager.software_manager = self.software_manager
|
self.session_manager.software_manager = self.software_manager
|
||||||
|
self._install_system_software()
|
||||||
|
|
||||||
def _init_action_manager(self) -> ActionManager:
|
def _init_action_manager(self) -> ActionManager:
|
||||||
# TODO: I see that this code is really confusing and hard to read right now... I think some of these things will
|
# TODO: I see that this code is really confusing and hard to read right now... I think some of these things will
|
||||||
@@ -971,6 +974,10 @@ class Node(SimComponent):
|
|||||||
|
|
||||||
return am
|
return am
|
||||||
|
|
||||||
|
def _install_system_software(self):
|
||||||
|
"""Install System Software - software that is usually provided with the OS."""
|
||||||
|
pass
|
||||||
|
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
Produce a dictionary describing the current state of this object.
|
Produce a dictionary describing the current state of this object.
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from primaite.simulator.network.hardware.base import NIC, Node
|
from primaite.simulator.network.hardware.base import NIC, Node
|
||||||
|
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||||
|
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||||
|
|
||||||
|
|
||||||
class Computer(Node):
|
class Computer(Node):
|
||||||
@@ -36,3 +38,14 @@ class Computer(Node):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.connect_nic(NIC(ip_address=kwargs["ip_address"], subnet_mask=kwargs["subnet_mask"]))
|
self.connect_nic(NIC(ip_address=kwargs["ip_address"], subnet_mask=kwargs["subnet_mask"]))
|
||||||
|
self._install_system_software()
|
||||||
|
|
||||||
|
def _install_system_software(self):
|
||||||
|
"""Install System Software - software that is usually provided with the OS."""
|
||||||
|
# DNS Client
|
||||||
|
self.software_manager.install(DNSClient)
|
||||||
|
|
||||||
|
# FTP
|
||||||
|
self.software_manager.install(FTPClient)
|
||||||
|
|
||||||
|
super()._install_system_software()
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from primaite.simulator.network.hardware.nodes.switch import Switch
|
|||||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
from primaite.simulator.network.transmission.transport_layer import Port
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||||
from primaite.simulator.system.services.database_service import DatabaseService
|
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||||
from primaite.simulator.system.services.dns_client import DNSClient
|
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||||
from primaite.simulator.system.services.dns_server import DNSServer
|
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
||||||
from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot
|
from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot
|
||||||
|
|
||||||
|
|
||||||
@@ -135,9 +135,6 @@ def arcd_uc2_network() -> Network:
|
|||||||
dns_server=IPv4Address("192.168.1.10"),
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
client_1.power_on()
|
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])
|
network.connect(endpoint_b=client_1.ethernet_port[1], endpoint_a=switch_2.switch_ports[1])
|
||||||
client_1.software_manager.install(DataManipulationBot)
|
client_1.software_manager.install(DataManipulationBot)
|
||||||
db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"]
|
db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"]
|
||||||
@@ -152,9 +149,6 @@ def arcd_uc2_network() -> Network:
|
|||||||
dns_server=IPv4Address("192.168.1.10"),
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
client_2.power_on()
|
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])
|
network.connect(endpoint_b=client_2.ethernet_port[1], endpoint_a=switch_2.switch_ports[2])
|
||||||
|
|
||||||
# Domain Controller
|
# Domain Controller
|
||||||
@@ -191,24 +185,53 @@ def arcd_uc2_network() -> Network:
|
|||||||
);"""
|
);"""
|
||||||
|
|
||||||
user_insert_statements = [
|
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) "
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');", # noqa
|
"VALUES ('John Doe', 'johndoe@example.com', 32, 'New York', 'Engineer');",
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');", # noqa
|
# 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) "
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('David Kim', 'davidkim@example.com', 38, 'Houston', 'Consultant');", # noqa
|
"VALUES ('Jane Smith', 'janesmith@example.com', 27, 'Los Angeles', 'Designer');",
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Emily Chen', 'emilychen@example.com', 29, 'Seattle', 'Software Developer');", # noqa
|
# 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) "
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Grace Park', 'gracepark@example.com', 31, 'Los Angeles', 'Marketing Specialist');", # noqa
|
"VALUES ('Bob Johnson', 'bobjohnson@example.com', 45, 'Chicago', 'Manager');",
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Henry Wu', 'henrywu@example.com', 40, 'Chicago', 'Accountant');", # noqa
|
# 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) "
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Jake Lee', 'jakelee@example.com', 33, 'Houston', 'Sales Manager');", # noqa
|
"VALUES ('Alice Lee', 'alicelee@example.com', 22, 'San Francisco', 'Student');",
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Kelly Chen', 'kellychen@example.com', 28, 'Seattle', 'Web Developer');", # noqa
|
# 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) "
|
||||||
"INSERT INTO user (name, email, age, city, occupation) VALUES ('Maggie Wang', 'maggiewang@example.com', 30, 'Los Angeles', 'Data Analyst');", # noqa
|
"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_server.software_manager.install(DatabaseService)
|
||||||
database_service: DatabaseService = database_server.software_manager.software["DatabaseService"] # noqa
|
database_service: DatabaseService = database_server.software_manager.software["DatabaseService"] # noqa
|
||||||
database_service.start()
|
database_service.start()
|
||||||
|
database_service.configure_backup(backup_server=IPv4Address("192.168.1.16"))
|
||||||
database_service._process_sql(ddl, None) # noqa
|
database_service._process_sql(ddl, None) # noqa
|
||||||
for insert_statement in user_insert_statements:
|
for insert_statement in user_insert_statements:
|
||||||
database_service._process_sql(insert_statement, None) # noqa
|
database_service._process_sql(insert_statement, None) # noqa
|
||||||
@@ -232,7 +255,6 @@ def arcd_uc2_network() -> Network:
|
|||||||
|
|
||||||
# register the web_server to a domain
|
# register the web_server to a domain
|
||||||
dns_server_service: DNSServer = domain_controller.software_manager.software["DNSServer"] # noqa
|
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)
|
dns_server_service.dns_register("arcd.com", web_server.ip_address)
|
||||||
|
|
||||||
# Backup Server
|
# Backup Server
|
||||||
@@ -246,6 +268,8 @@ def arcd_uc2_network() -> Network:
|
|||||||
backup_server.power_on()
|
backup_server.power_on()
|
||||||
network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4])
|
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
|
||||||
security_suite = Server(
|
security_suite = Server(
|
||||||
hostname="security_suite",
|
hostname="security_suite",
|
||||||
@@ -271,4 +295,7 @@ def arcd_uc2_network() -> Network:
|
|||||||
# Allow DNS requests
|
# Allow DNS requests
|
||||||
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.DNS, dst_port=Port.DNS, position=1)
|
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
|
return network
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from typing import Optional
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from primaite.simulator.network.protocols.packet import DataPacket
|
||||||
|
|
||||||
|
|
||||||
class ARPEntry(BaseModel):
|
class ARPEntry(BaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -18,7 +20,7 @@ class ARPEntry(BaseModel):
|
|||||||
nic_uuid: str
|
nic_uuid: str
|
||||||
|
|
||||||
|
|
||||||
class ARPPacket(BaseModel):
|
class ARPPacket(DataPacket):
|
||||||
"""
|
"""
|
||||||
Represents the ARP layer of a network frame.
|
Represents the ARP layer of a network frame.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from typing import Optional
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from primaite.simulator.network.protocols.packet import DataPacket
|
||||||
|
|
||||||
|
|
||||||
class DNSRequest(BaseModel):
|
class DNSRequest(BaseModel):
|
||||||
"""Represents a DNS Request packet of a network frame.
|
"""Represents a DNS Request packet of a network frame.
|
||||||
@@ -26,7 +28,7 @@ class DNSReply(BaseModel):
|
|||||||
"IP Address of the Domain Name requested."
|
"IP Address of the Domain Name requested."
|
||||||
|
|
||||||
|
|
||||||
class DNSPacket(BaseModel):
|
class DNSPacket(DataPacket):
|
||||||
"""
|
"""
|
||||||
Represents the DNS layer of a network frame.
|
Represents the DNS layer of a network frame.
|
||||||
|
|
||||||
|
|||||||
55
src/primaite/simulator/network/protocols/ftp.py
Normal file
55
src/primaite/simulator/network/protocols/ftp.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
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: Optional[Any] = None
|
||||||
|
"""Arguments for command."""
|
||||||
|
|
||||||
|
status_code: FTPStatusCode = None
|
||||||
|
"""Status of the response."""
|
||||||
12
src/primaite/simulator/network/protocols/packet.py
Normal file
12
src/primaite/simulator/network/protocols/packet.py
Normal file
@@ -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")))
|
||||||
@@ -5,6 +5,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from primaite import getLogger
|
from primaite import getLogger
|
||||||
from primaite.simulator.network.protocols.arp import ARPPacket
|
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.network_layer import ICMPPacket, IPPacket, IPProtocol
|
||||||
from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader
|
from primaite.simulator.network.transmission.primaite_layer import PrimaiteHeader
|
||||||
from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader
|
from primaite.simulator.network.transmission.transport_layer import TCPHeader, UDPHeader
|
||||||
@@ -132,6 +133,10 @@ class Frame(BaseModel):
|
|||||||
@property
|
@property
|
||||||
def size(self) -> float: # noqa - Keep it as MBits as this is how they're expressed
|
def size(self) -> float: # noqa - Keep it as MBits as this is how they're expressed
|
||||||
"""The size of the Frame in Bytes."""
|
"""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")))
|
return float(len(self.model_dump_json().encode("utf-8")))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from ipaddress import IPv4Address
|
||||||
from sqlite3 import OperationalError
|
from sqlite3 import OperationalError
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ from primaite.simulator.file_system.file_system import File
|
|||||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
from primaite.simulator.network.transmission.transport_layer import Port
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
from primaite.simulator.system.core.software_manager import SoftwareManager
|
from primaite.simulator.system.core.software_manager import SoftwareManager
|
||||||
|
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||||
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
||||||
from primaite.simulator.system.software import SoftwareHealthState
|
from primaite.simulator.system.software import SoftwareHealthState
|
||||||
|
|
||||||
@@ -23,6 +25,15 @@ class DatabaseService(Service):
|
|||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
connections: Dict[str, datetime] = {}
|
connections: Dict[str, datetime] = {}
|
||||||
|
|
||||||
|
backup_server: IPv4Address = None
|
||||||
|
"""IP address of the backup server."""
|
||||||
|
|
||||||
|
latest_backup_directory: str = None
|
||||||
|
"""Directory of latest backup."""
|
||||||
|
|
||||||
|
latest_backup_file_name: str = None
|
||||||
|
"""File name of latest backup."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
kwargs["name"] = "DatabaseService"
|
kwargs["name"] = "DatabaseService"
|
||||||
kwargs["port"] = Port.POSTGRES_SERVER
|
kwargs["port"] = Port.POSTGRES_SERVER
|
||||||
@@ -30,6 +41,9 @@ class DatabaseService(Service):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._db_file: File
|
self._db_file: File
|
||||||
self._create_db_file()
|
self._create_db_file()
|
||||||
|
self._connect()
|
||||||
|
|
||||||
|
def _connect(self):
|
||||||
self._conn = sqlite3.connect(self._db_file.sim_path)
|
self._conn = sqlite3.connect(self._db_file.sim_path)
|
||||||
self._cursor = self._conn.cursor()
|
self._cursor = self._conn.cursor()
|
||||||
|
|
||||||
@@ -40,8 +54,10 @@ class DatabaseService(Service):
|
|||||||
:return: List of table names.
|
:return: List of table names.
|
||||||
"""
|
"""
|
||||||
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence';"
|
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name != 'sqlite_sequence';"
|
||||||
results = self._process_sql(sql)
|
results = self._process_sql(sql, None)
|
||||||
return [row[0] for row in results["data"]]
|
if isinstance(results["data"], dict):
|
||||||
|
return list(results["data"].keys())
|
||||||
|
return []
|
||||||
|
|
||||||
def show(self, markdown: bool = False):
|
def show(self, markdown: bool = False):
|
||||||
"""
|
"""
|
||||||
@@ -58,6 +74,72 @@ class DatabaseService(Service):
|
|||||||
table.add_row([row])
|
table.add_row([row])
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
|
def configure_backup(self, backup_server: IPv4Address):
|
||||||
|
"""
|
||||||
|
Set up the database backup.
|
||||||
|
|
||||||
|
:param: backup_server_ip: The IP address of the backup server
|
||||||
|
"""
|
||||||
|
self.backup_server = backup_server
|
||||||
|
|
||||||
|
def backup_database(self) -> bool:
|
||||||
|
"""Create a backup of the database to the configured backup server."""
|
||||||
|
# check if the backup server was configured
|
||||||
|
if self.backup_server is None:
|
||||||
|
self.sys_log.error(f"{self.name} - {self.sys_log.hostname}: not configured.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._conn.close()
|
||||||
|
|
||||||
|
software_manager: SoftwareManager = self.software_manager
|
||||||
|
ftp_client_service: FTPClient = software_manager.software["FTPClient"]
|
||||||
|
|
||||||
|
# send backup copy of database file to FTP server
|
||||||
|
response = ftp_client_service.send_file(
|
||||||
|
dest_ip_address=self.backup_server,
|
||||||
|
src_file_name=self._db_file.name,
|
||||||
|
src_folder_name=self._db_file.folder.name,
|
||||||
|
dest_folder_name=str(self.uuid),
|
||||||
|
dest_file_name="database.db",
|
||||||
|
real_file_path=self._db_file.sim_path,
|
||||||
|
)
|
||||||
|
self._connect()
|
||||||
|
|
||||||
|
if response:
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.sys_log.error("Unable to create database backup.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def restore_backup(self) -> bool:
|
||||||
|
"""Restore a backup from backup server."""
|
||||||
|
software_manager: SoftwareManager = self.software_manager
|
||||||
|
ftp_client_service: FTPClient = software_manager.software["FTPClient"]
|
||||||
|
|
||||||
|
# retrieve backup file from backup server
|
||||||
|
response = ftp_client_service.request_file(
|
||||||
|
src_folder_name=str(self.uuid),
|
||||||
|
src_file_name="database.db",
|
||||||
|
dest_folder_name="downloads",
|
||||||
|
dest_file_name="database.db",
|
||||||
|
dest_ip_address=self.backup_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
self._conn.close()
|
||||||
|
# replace db file
|
||||||
|
self.file_system.delete_file(folder_name=self.folder.name, file_name="downloads.db")
|
||||||
|
self.file_system.move_file(
|
||||||
|
src_folder_name="downloads", src_file_name="database.db", dst_folder_name=self.folder.name
|
||||||
|
)
|
||||||
|
self._db_file = self.file_system.get_file(folder_name=self.folder.name, file_name="database.db")
|
||||||
|
self._connect()
|
||||||
|
|
||||||
|
return self._db_file is not None
|
||||||
|
|
||||||
|
self.sys_log.error("Unable to restore database backup.")
|
||||||
|
return False
|
||||||
|
|
||||||
def _create_db_file(self):
|
def _create_db_file(self):
|
||||||
"""Creates the Simulation File and sqlite file in the file system."""
|
"""Creates the Simulation File and sqlite file in the file system."""
|
||||||
self._db_file: File = self.file_system.create_file(folder_name="database", file_name="database.db", real=True)
|
self._db_file: File = self.file_system.create_file(folder_name="database", file_name="database.db", real=True)
|
||||||
@@ -27,6 +27,7 @@ class DNSClient(Service):
|
|||||||
# TCP for now
|
# TCP for now
|
||||||
kwargs["protocol"] = IPProtocol.TCP
|
kwargs["protocol"] = IPProtocol.TCP
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self.start()
|
||||||
|
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
@@ -26,6 +26,7 @@ class DNSServer(Service):
|
|||||||
# TCP for now
|
# TCP for now
|
||||||
kwargs["protocol"] = IPProtocol.TCP
|
kwargs["protocol"] = IPProtocol.TCP
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
self.start()
|
||||||
|
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
256
src/primaite/simulator/system/services/ftp/ftp_client.py
Normal file
256
src/primaite/simulator/system/services/ftp/ftp_client.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
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.ftp.ftp_service import FTPServiceABC
|
||||||
|
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||||
|
|
||||||
|
|
||||||
|
class FTPClient(FTPServiceABC):
|
||||||
|
"""
|
||||||
|
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 _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]
|
||||||
|
"""
|
||||||
|
# if client service is down, return error
|
||||||
|
if self.operating_state != ServiceOperatingState.RUNNING:
|
||||||
|
payload.status_code = FTPStatusCode.ERROR
|
||||||
|
return payload
|
||||||
|
|
||||||
|
# process client specific commands, otherwise call super
|
||||||
|
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,
|
||||||
|
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: dest_ip_address: Optional[IPv4Address]
|
||||||
|
:param: dest_port: Port of the FTP server the client needs to connect to. Optional.
|
||||||
|
: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]
|
||||||
|
"""
|
||||||
|
# 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,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
software_manager.send_payload_to_session_manager(
|
||||||
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port
|
||||||
|
)
|
||||||
|
if payload.status_code == FTPStatusCode.OK:
|
||||||
|
self.connected = False
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
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,
|
||||||
|
real_file_path: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
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}")
|
||||||
|
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:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.sys_log.info(f"Sending file {src_folder_name}/{src_file_name} to {str(dest_ip_address)}")
|
||||||
|
# send STOR request
|
||||||
|
if self._send_data(
|
||||||
|
file=file_to_transfer,
|
||||||
|
dest_folder_name=dest_folder_name,
|
||||||
|
dest_file_name=dest_file_name,
|
||||||
|
dest_ip_address=dest_ip_address,
|
||||||
|
dest_port=dest_port,
|
||||||
|
):
|
||||||
|
return self._disconnect_from_server(dest_ip_address=dest_ip_address, dest_port=dest_port)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
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,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
dest_port=dest_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.connected:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# send retrieve request
|
||||||
|
payload: FTPPacket = FTPPacket(
|
||||||
|
ftp_command=FTPCommand.RETR,
|
||||||
|
ftp_command_args={
|
||||||
|
"src_folder_name": src_folder_name,
|
||||||
|
"src_file_name": src_file_name,
|
||||||
|
"dest_file_name": dest_file_name,
|
||||||
|
"dest_folder_name": dest_folder_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.sys_log.info(f"Requesting file {src_folder_name}/{src_file_name} from {str(dest_ip_address)}")
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# the payload should have ok status code
|
||||||
|
if payload.status_code == FTPStatusCode.OK:
|
||||||
|
self.sys_log.info(f"File {src_folder_name}/{src_file_name} found in FTP server.")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.sys_log.error(f"File {src_folder_name}/{src_file_name} does not exist in FTP server")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
self._process_ftp_command(payload=payload, session_id=session_id)
|
||||||
|
return True
|
||||||
83
src/primaite/simulator/system/services/ftp/ftp_server.py
Normal file
83
src/primaite/simulator/system/services/ftp/ftp_server.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from ipaddress import IPv4Address
|
||||||
|
from typing import Any, Dict, 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.core.session_manager import Session
|
||||||
|
from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC
|
||||||
|
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||||
|
|
||||||
|
|
||||||
|
class FTPServer(FTPServiceABC):
|
||||||
|
"""
|
||||||
|
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."""
|
||||||
|
|
||||||
|
connections: Dict[str, IPv4Address] = {}
|
||||||
|
"""Current active connections to the FTP server."""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs["name"] = "FTPServer"
|
||||||
|
kwargs["port"] = Port.FTP
|
||||||
|
kwargs["protocol"] = IPProtocol.TCP
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def _get_session_details(self, session_id: str) -> Session:
|
||||||
|
"""
|
||||||
|
Returns the Session object from the given session id.
|
||||||
|
|
||||||
|
:param: session_id: ID of the session that needs details retrieved
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
if session_id:
|
||||||
|
session_details = self._get_session_details(session_id)
|
||||||
|
|
||||||
|
if payload.ftp_command is not None:
|
||||||
|
self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
self.connections[session_id] = session_details.with_ip_address
|
||||||
|
payload.status_code = FTPStatusCode.OK
|
||||||
|
return payload
|
||||||
|
|
||||||
|
if payload.ftp_command == FTPCommand.QUIT:
|
||||||
|
self.connections.pop(session_id)
|
||||||
|
payload.status_code = FTPStatusCode.OK
|
||||||
|
|
||||||
|
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
|
||||||
|
|
||||||
|
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=session_id), session_id)
|
||||||
|
return True
|
||||||
155
src/primaite/simulator/system/services/ftp/ftp_service.py
Normal file
155
src/primaite/simulator/system/services/ftp/ftp_service.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import shutil
|
||||||
|
from abc import ABC
|
||||||
|
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.transport_layer import Port
|
||||||
|
from primaite.simulator.system.core.software_manager import SoftwareManager
|
||||||
|
from primaite.simulator.system.services.service import Service
|
||||||
|
|
||||||
|
|
||||||
|
class FTPServiceABC(Service, ABC):
|
||||||
|
"""
|
||||||
|
Abstract Base Class for FTP Client and Service.
|
||||||
|
|
||||||
|
Contains shared methods between both classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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]
|
||||||
|
"""
|
||||||
|
if payload.ftp_command is not None:
|
||||||
|
self.sys_log.info(f"Received FTP {payload.ftp_command.name} command.")
|
||||||
|
|
||||||
|
# handle STOR request
|
||||||
|
if payload.ftp_command == FTPCommand.STOR:
|
||||||
|
# check that the file is created in the computed hosting the FTP server
|
||||||
|
if self._store_data(payload=payload):
|
||||||
|
payload.status_code = FTPStatusCode.OK
|
||||||
|
|
||||||
|
if payload.ftp_command == FTPCommand.RETR:
|
||||||
|
if self._retrieve_data(payload=payload, session_id=session_id):
|
||||||
|
payload.status_code = FTPStatusCode.OK
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _store_data(self, payload: FTPPacket) -> bool:
|
||||||
|
"""
|
||||||
|
Stores the data in the FTP Service's host machine.
|
||||||
|
|
||||||
|
:param: payload: The FTP Packet that contains the file data
|
||||||
|
:type: FTPPacket
|
||||||
|
"""
|
||||||
|
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"]
|
||||||
|
real_file_path = payload.ftp_command_args.get("real_file_path")
|
||||||
|
is_real = real_file_path is not None
|
||||||
|
file = self.file_system.create_file(
|
||||||
|
file_name=file_name, folder_name=folder_name, size=file_size, real=is_real
|
||||||
|
)
|
||||||
|
self.sys_log.info(
|
||||||
|
f"Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/"
|
||||||
|
f"{payload.ftp_command_args['dest_file_name']}"
|
||||||
|
)
|
||||||
|
if is_real:
|
||||||
|
shutil.copy(real_file_path, file.sim_path)
|
||||||
|
# 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 create file in {self.sys_log.hostname}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _send_data(
|
||||||
|
self,
|
||||||
|
file: File,
|
||||||
|
dest_folder_name: str,
|
||||||
|
dest_file_name: str,
|
||||||
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
|
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,
|
||||||
|
ftp_command_args={
|
||||||
|
"dest_folder_name": dest_folder_name,
|
||||||
|
"dest_file_name": dest_file_name,
|
||||||
|
"file_size": file.sim_size,
|
||||||
|
"real_file_path": file.sim_path if file.real else None,
|
||||||
|
},
|
||||||
|
packet_payload_size=file.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, session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if payload.status_code == FTPStatusCode.OK:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _retrieve_data(self, payload: FTPPacket, session_id: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Handle the transfer of data from Server to Client.
|
||||||
|
|
||||||
|
:param: payload: The FTP Packet that contains the file data
|
||||||
|
:type: FTPPacket
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 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
|
||||||
|
if not retrieved_file:
|
||||||
|
self.sys_log.error(
|
||||||
|
f"File {payload.ftp_command_args['dest_folder_name']}/"
|
||||||
|
f"{payload.ftp_command_args['dest_file_name']} does not exist in {self.sys_log.hostname}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# send requested data
|
||||||
|
return self._send_data(
|
||||||
|
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}")
|
||||||
|
return False
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
from primaite.simulator.network.hardware.nodes.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||||
from primaite.simulator.system.services.database_service import DatabaseService
|
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||||
from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot
|
from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot
|
||||||
|
|
||||||
|
|
||||||
def test_data_manipulation(uc2_network):
|
def test_data_manipulation(uc2_network):
|
||||||
|
"""Tests the UC2 data manipulation scenario end-to-end. Is a work in progress."""
|
||||||
client_1: Computer = uc2_network.get_node_by_hostname("client_1")
|
client_1: Computer = uc2_network.get_node_by_hostname("client_1")
|
||||||
db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"]
|
db_manipulation_bot: DataManipulationBot = client_1.software_manager.software["DataManipulationBot"]
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ def test_data_manipulation(uc2_network):
|
|||||||
web_server: Server = uc2_network.get_node_by_hostname("web_server")
|
web_server: Server = uc2_network.get_node_by_hostname("web_server")
|
||||||
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"]
|
db_client: DatabaseClient = web_server.software_manager.software["DatabaseClient"]
|
||||||
|
|
||||||
|
db_service.backup_database()
|
||||||
|
|
||||||
# First check that the DB client on the web_server can successfully query the users table on the database
|
# First check that the DB client on the web_server can successfully query the users table on the database
|
||||||
assert db_client.query("SELECT * FROM user;")
|
assert db_client.query("SELECT * FROM user;")
|
||||||
|
|
||||||
@@ -23,3 +26,9 @@ def test_data_manipulation(uc2_network):
|
|||||||
|
|
||||||
# Now check that the DB client on the web_server cannot query the users table on the database
|
# Now check that the DB client on the web_server cannot query the users table on the database
|
||||||
assert not db_client.query("SELECT * FROM user;")
|
assert not db_client.query("SELECT * FROM user;")
|
||||||
|
|
||||||
|
# Now restore the database
|
||||||
|
db_service.restore_backup()
|
||||||
|
|
||||||
|
# Now check that the DB client on the web_server can successfully query the users table on the database
|
||||||
|
assert db_client.query("SELECT * FROM user;")
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from ipaddress import IPv4Address
|
|||||||
|
|
||||||
from primaite.simulator.network.hardware.nodes.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.system.applications.database_client import DatabaseClient
|
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||||
from primaite.simulator.system.services.database_service import DatabaseService
|
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||||
|
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
||||||
|
|
||||||
|
|
||||||
def test_database_client_server_connection(uc2_network):
|
def test_database_client_server_connection(uc2_network):
|
||||||
@@ -57,3 +58,37 @@ def test_database_client_query(uc2_network):
|
|||||||
db_client.connect()
|
db_client.connect()
|
||||||
|
|
||||||
assert db_client.query("SELECT * FROM user;")
|
assert db_client.query("SELECT * FROM user;")
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_database_backup(uc2_network):
|
||||||
|
"""Run the backup_database method and check if the FTP server has the relevant file."""
|
||||||
|
db_server: Server = uc2_network.get_node_by_hostname("database_server")
|
||||||
|
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"]
|
||||||
|
|
||||||
|
# back up should be created
|
||||||
|
assert db_service.backup_database() is True
|
||||||
|
|
||||||
|
backup_server: Server = uc2_network.get_node_by_hostname("backup_server")
|
||||||
|
ftp_server: FTPServer = backup_server.software_manager.software["FTPServer"]
|
||||||
|
|
||||||
|
# backup file should exist in the backup server
|
||||||
|
assert ftp_server.file_system.get_file(folder_name=db_service.uuid, file_name="database.db") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_restore_backup(uc2_network):
|
||||||
|
"""Run the restore_backup method and check if the backup is properly restored."""
|
||||||
|
db_server: Server = uc2_network.get_node_by_hostname("database_server")
|
||||||
|
db_service: DatabaseService = db_server.software_manager.software["DatabaseService"]
|
||||||
|
|
||||||
|
# create a back up
|
||||||
|
assert db_service.backup_database() is True
|
||||||
|
|
||||||
|
# delete database locally
|
||||||
|
db_service.file_system.delete_file(folder_name="database", file_name="database.db")
|
||||||
|
|
||||||
|
assert db_service.file_system.get_file(folder_name="database", file_name="database.db") is None
|
||||||
|
|
||||||
|
# back up should be restored
|
||||||
|
assert db_service.restore_backup() is True
|
||||||
|
|
||||||
|
assert db_service.file_system.get_file(folder_name="database", file_name="database.db") is not None
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from ipaddress import IPv4Address
|
|
||||||
|
|
||||||
from primaite.simulator.network.hardware.nodes.computer import Computer
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
from primaite.simulator.network.hardware.nodes.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.system.services.dns_client import DNSClient
|
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||||
from primaite.simulator.system.services.dns_server import DNSServer
|
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
62
tests/integration_tests/system/test_ftp_client_server.py
Normal file
62
tests/integration_tests/system/test_ftp_client_server.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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")
|
||||||
|
|
||||||
|
assert 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")
|
||||||
|
|
||||||
|
assert 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# client should have retrieved the file
|
||||||
|
assert ftp_client.file_system.get_file(folder_name="downloads", file_name="test_file.txt")
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from primaite.simulator.network.hardware.base import Node
|
from primaite.simulator.network.hardware.base import Node
|
||||||
from primaite.simulator.system.services.database_service import DatabaseService
|
from primaite.simulator.system.services.database.database_service import DatabaseService
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
|
|||||||
@@ -3,16 +3,20 @@ from ipaddress import IPv4Address
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from primaite.simulator.network.hardware.base import Node
|
from primaite.simulator.network.hardware.base import Node
|
||||||
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest
|
from primaite.simulator.network.protocols.dns import DNSPacket, DNSReply, DNSRequest
|
||||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
from primaite.simulator.network.transmission.transport_layer import Port
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
from primaite.simulator.system.services.dns_client import DNSClient
|
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||||
from primaite.simulator.system.services.dns_server import DNSServer
|
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def dns_server() -> Node:
|
def dns_server() -> Node:
|
||||||
node = Node(hostname="dns_server")
|
node = Server(
|
||||||
|
hostname="dns_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
||||||
|
)
|
||||||
node.software_manager.install(software_class=DNSServer)
|
node.software_manager.install(software_class=DNSServer)
|
||||||
node.software_manager.software["DNSServer"].start()
|
node.software_manager.software["DNSServer"].start()
|
||||||
return node
|
return node
|
||||||
@@ -20,9 +24,9 @@ def dns_server() -> Node:
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def dns_client() -> Node:
|
def dns_client() -> Node:
|
||||||
node = Node(hostname="dns_client")
|
node = Computer(
|
||||||
node.software_manager.install(software_class=DNSClient)
|
hostname="dns_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
||||||
node.software_manager.software["DNSClient"].start()
|
)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from primaite.simulator.network.hardware.base import Node
|
||||||
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
|
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket
|
||||||
|
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 = Server(
|
||||||
|
hostname="ftp_server", ip_address="192.168.1.10", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
||||||
|
)
|
||||||
|
node.software_manager.install(software_class=FTPServer)
|
||||||
|
node.software_manager.software["FTPServer"].start()
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def ftp_client() -> Node:
|
||||||
|
node = Computer(
|
||||||
|
hostname="ftp_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_ftp_server_store_file(ftp_server):
|
||||||
|
"""Test to make sure the FTP Server knows how to deal with request responses."""
|
||||||
|
assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt") is None
|
||||||
|
|
||||||
|
response: FTPPacket = FTPPacket(
|
||||||
|
ftp_command=FTPCommand.STOR,
|
||||||
|
ftp_command_args={
|
||||||
|
"dest_folder_name": "downloads",
|
||||||
|
"dest_file_name": "file.txt",
|
||||||
|
"file_size": 24,
|
||||||
|
},
|
||||||
|
packet_payload_size=24,
|
||||||
|
)
|
||||||
|
|
||||||
|
ftp_server_service: FTPServer = ftp_server.software_manager.software["FTPServer"]
|
||||||
|
ftp_server_service.receive(response)
|
||||||
|
|
||||||
|
assert ftp_server.file_system.get_file(folder_name="downloads", file_name="file.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ftp_client_store_file(ftp_client):
|
||||||
|
"""Test to make sure the FTP Client knows how to deal with request responses."""
|
||||||
|
assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt") is None
|
||||||
|
|
||||||
|
response: FTPPacket = FTPPacket(
|
||||||
|
ftp_command=FTPCommand.STOR,
|
||||||
|
ftp_command_args={
|
||||||
|
"dest_folder_name": "downloads",
|
||||||
|
"dest_file_name": "file.txt",
|
||||||
|
"file_size": 24,
|
||||||
|
},
|
||||||
|
packet_payload_size=24,
|
||||||
|
)
|
||||||
|
|
||||||
|
ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"]
|
||||||
|
ftp_client_service.receive(response)
|
||||||
|
|
||||||
|
assert ftp_client.file_system.get_file(folder_name="downloads", file_name="file.txt")
|
||||||
Reference in New Issue
Block a user