Merge remote-tracking branch 'origin/dev' into feature/1924-Agent-Interface
This commit is contained in:
@@ -34,6 +34,7 @@ SessionManager.
|
|||||||
- 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: `DNSClient` and `DNSServer`
|
- DNS Services: `DNSClient` and `DNSServer`
|
||||||
- FTP Services: `FTPClient` and `FTPServer`
|
- FTP Services: `FTPClient` and `FTPServer`
|
||||||
|
- HTTP Services: `WebBrowser` to simulate a web client and `WebServer`
|
||||||
|
|
||||||
## [2.0.0] - 2023-07-26
|
## [2.0.0] - 2023-07-26
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Implementation
|
|||||||
|
|
||||||
|
|
||||||
Example Usage
|
Example Usage
|
||||||
----------
|
-------------
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ Contents
|
|||||||
data_manipulation_bot
|
data_manipulation_bot
|
||||||
dns_client_server
|
dns_client_server
|
||||||
ftp_client_server
|
ftp_client_server
|
||||||
|
web_browser_and_web_server_service
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
.. only:: comment
|
||||||
|
|
||||||
|
© Crown-owned copyright 2023, Defence Science and Technology Laboratory UK
|
||||||
|
|
||||||
|
Web Browser and Web Server Service
|
||||||
|
==================================
|
||||||
|
|
||||||
|
Web Server Service
|
||||||
|
------------------
|
||||||
|
Provides a Web Server simulation by extending the base Service class.
|
||||||
|
|
||||||
|
Key capabilities
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Simulates a web server with the capability to also request data from a database
|
||||||
|
- Allows the emulation of HTTP requests between client (e.g. a web browser) and server
|
||||||
|
- GET request sends a get all users request to the database server and returns an HTTP 200 status if the database is responsive
|
||||||
|
- Leverages the Service base class for install/uninstall, status tracking, etc.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
^^^^^
|
||||||
|
- Install on a Node via the ``SoftwareManager`` to start the `WebServer`.
|
||||||
|
- Service runs on HTTP port 80 by default. (TODO: HTTPS)
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- HTTP request uses a ``HttpRequestPacket`` object
|
||||||
|
- HTTP response uses a ``HttpResponsePacket`` object
|
||||||
|
- Extends Service class for integration with ``SoftwareManager``.
|
||||||
|
|
||||||
|
Web Browser (Web Client)
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The ``WebBrowser`` provides a client interface for connecting to the ``WebServer``.
|
||||||
|
|
||||||
|
Key features
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Connects to the ``WebServer`` via the ``SoftwareManager``.
|
||||||
|
- Simulates HTTP requests and HTTP packet transfer across a network
|
||||||
|
- Allows the emulation of HTTP requests between client and server:
|
||||||
|
- Automatically uses ``DNSClient`` to resolve domain names
|
||||||
|
- GET: performs an HTTP GET request from client to server
|
||||||
|
- Leverages the Service base class for install/uninstall, status tracking, etc.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
- Install on a Node via the ``SoftwareManager`` to start the ``WebBrowser``.
|
||||||
|
- Service runs on HTTP port 80 by default. (TODO: HTTPS)
|
||||||
|
- Execute sending an HTTP GET request with ``get_webpage``
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
- Leverages ``SoftwareManager`` for sending payloads over the network.
|
||||||
|
- Provides easy interface for making HTTP requests between an HTTP client and server.
|
||||||
|
- Extends base Service class.
|
||||||
|
|
||||||
|
|
||||||
|
Example Usage
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
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.applications.web_browser import WebBrowser
|
||||||
|
from primaite.simulator.system.services.web_server.web_server_service import WebServer
|
||||||
|
|
||||||
|
Example peer to peer network
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
net = Network()
|
||||||
|
|
||||||
|
pc1 = Computer(hostname="pc1", ip_address="192.168.1.50", subnet_mask="255.255.255.0")
|
||||||
|
srv = Server(hostname="srv", ip_address="192.168.1.10", subnet_mask="255.255.255.0")
|
||||||
|
pc1.power_on()
|
||||||
|
srv.power_on()
|
||||||
|
net.connect(pc1.ethernet_port[1], srv.ethernet_port[1])
|
||||||
|
|
||||||
|
Install the Web Server
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# web browser is automatically installed in computer nodes
|
||||||
|
# IRL this is usually included with an OS
|
||||||
|
client: WebBrowser = pc1.software_manager.software['WebBrowser']
|
||||||
|
|
||||||
|
# install web server
|
||||||
|
srv.software_manager.install(WebServer)
|
||||||
|
webserv: WebServer = srv.software_manager.software['WebServer']
|
||||||
|
|
||||||
|
Open the web page
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Using a domain name to connect to a website requires setting up DNS Servers. For this example, it is possible to use the IP address directly
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# check that the get request succeeded
|
||||||
|
print(client.get_webpage("http://192.168.1.10")) # should be True
|
||||||
@@ -722,7 +722,9 @@ class ARPCache:
|
|||||||
|
|
||||||
# Unmatched ARP Request
|
# Unmatched ARP Request
|
||||||
if arp_packet.target_ip_address != from_nic.ip_address:
|
if arp_packet.target_ip_address != from_nic.ip_address:
|
||||||
self.sys_log.info(f"Ignoring ARP request for {arp_packet.target_ip_address}")
|
self.sys_log.info(
|
||||||
|
f"Ignoring ARP request for {arp_packet.target_ip_address}. Current IP address is {from_nic.ip_address}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Matched ARP request
|
# Matched ARP request
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from primaite.simulator.network.hardware.base import NIC, Node
|
from primaite.simulator.network.hardware.base import NIC, Node
|
||||||
|
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||||
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||||
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||||
|
|
||||||
@@ -48,4 +49,7 @@ class Computer(Node):
|
|||||||
# FTP
|
# FTP
|
||||||
self.software_manager.install(FTPClient)
|
self.software_manager.install(FTPClient)
|
||||||
|
|
||||||
|
# Web Browser
|
||||||
|
self.software_manager.install(WebBrowser)
|
||||||
|
|
||||||
super()._install_system_software()
|
super()._install_system_software()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from primaite.simulator.system.services.database.database_service import Databas
|
|||||||
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
from primaite.simulator.system.services.dns.dns_server import DNSServer
|
||||||
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
|
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
|
||||||
|
from primaite.simulator.system.services.web_server.web_server import WebServer
|
||||||
|
|
||||||
|
|
||||||
def client_server_routed() -> Network:
|
def client_server_routed() -> Network:
|
||||||
@@ -253,6 +254,8 @@ def arcd_uc2_network() -> Network:
|
|||||||
database_client.run()
|
database_client.run()
|
||||||
database_client.connect()
|
database_client.connect()
|
||||||
|
|
||||||
|
web_server.software_manager.install(WebServer)
|
||||||
|
|
||||||
# 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.dns_register("arcd.com", web_server.ip_address)
|
dns_server_service.dns_register("arcd.com", web_server.ip_address)
|
||||||
@@ -266,9 +269,8 @@ def arcd_uc2_network() -> Network:
|
|||||||
dns_server=IPv4Address("192.168.1.10"),
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
backup_server.power_on()
|
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)
|
backup_server.software_manager.install(FTPServer)
|
||||||
|
network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4])
|
||||||
|
|
||||||
# Security Suite
|
# Security Suite
|
||||||
security_suite = Server(
|
security_suite = Server(
|
||||||
@@ -298,4 +300,7 @@ def arcd_uc2_network() -> Network:
|
|||||||
# Allow FTP requests
|
# Allow FTP requests
|
||||||
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2)
|
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.FTP, dst_port=Port.FTP, position=2)
|
||||||
|
|
||||||
|
# Open port 80 for web server
|
||||||
|
router_1.acl.add_rule(action=ACLAction.PERMIT, src_port=Port.HTTP, dst_port=Port.HTTP, position=3)
|
||||||
|
|
||||||
return network
|
return network
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from primaite.simulator.network.protocols.packet import DataPacket
|
from primaite.simulator.network.protocols.packet import DataPacket
|
||||||
|
|
||||||
@@ -51,5 +51,5 @@ class FTPPacket(DataPacket):
|
|||||||
ftp_command_args: Optional[Any] = None
|
ftp_command_args: Optional[Any] = None
|
||||||
"""Arguments for command."""
|
"""Arguments for command."""
|
||||||
|
|
||||||
status_code: FTPStatusCode = None
|
status_code: Union[FTPStatusCode, None] = None
|
||||||
"""Status of the response."""
|
"""Status of the response."""
|
||||||
|
|||||||
64
src/primaite/simulator/network/protocols/http.py
Normal file
64
src/primaite/simulator/network/protocols/http.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from primaite.simulator.network.protocols.packet import DataPacket
|
||||||
|
|
||||||
|
|
||||||
|
class HttpRequestMethod(Enum):
|
||||||
|
"""Enum list of HTTP Request methods that can be handled by the simulation."""
|
||||||
|
|
||||||
|
GET = "GET"
|
||||||
|
"""HTTP GET Method. Requests using GET should only retrieve data."""
|
||||||
|
|
||||||
|
HEAD = "HEAD"
|
||||||
|
"""Asks for a response identical to a GET request, but without the response body."""
|
||||||
|
|
||||||
|
POST = "POST"
|
||||||
|
"""Submit an entity to the specified resource, often causing a change in state or side effects on the server."""
|
||||||
|
|
||||||
|
PUT = "PUT"
|
||||||
|
"""Replace all current representations of the target resource with the request payload."""
|
||||||
|
|
||||||
|
DELETE = "DELETE"
|
||||||
|
"""Delete the specified resource."""
|
||||||
|
|
||||||
|
PATCH = "PATCH"
|
||||||
|
"""Apply partial modifications to a resource."""
|
||||||
|
|
||||||
|
|
||||||
|
class HttpStatusCode(Enum):
|
||||||
|
"""List of available HTTP Statuses."""
|
||||||
|
|
||||||
|
OK = 200
|
||||||
|
"""request has succeeded."""
|
||||||
|
|
||||||
|
BAD_REQUEST = 400
|
||||||
|
"""Payload cannot be parsed."""
|
||||||
|
|
||||||
|
UNAUTHORIZED = 401
|
||||||
|
"""Auth required."""
|
||||||
|
|
||||||
|
NOT_FOUND = 404
|
||||||
|
"""Item not found in server."""
|
||||||
|
|
||||||
|
METHOD_NOT_ALLOWED = 405
|
||||||
|
"""Method is not supported by server."""
|
||||||
|
|
||||||
|
INTERNAL_SERVER_ERROR = 500
|
||||||
|
"""Error on the server side."""
|
||||||
|
|
||||||
|
|
||||||
|
class HttpRequestPacket(DataPacket):
|
||||||
|
"""Class that represents an HTTP Request Packet."""
|
||||||
|
|
||||||
|
request_method: HttpRequestMethod
|
||||||
|
"""The HTTP Request method."""
|
||||||
|
|
||||||
|
request_url: str
|
||||||
|
"""URL of request."""
|
||||||
|
|
||||||
|
|
||||||
|
class HttpResponsePacket(DataPacket):
|
||||||
|
"""Class that reprensents an HTTP Response Packet."""
|
||||||
|
|
||||||
|
status_code: HttpStatusCode = None
|
||||||
|
"""Status code of the HTTP response."""
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class DataPacket(BaseModel):
|
class DataPacket(BaseModel):
|
||||||
"""Data packet abstract class."""
|
"""Data packet abstract class."""
|
||||||
|
|
||||||
|
payload: Any = None
|
||||||
|
"""Payload content of the packet."""
|
||||||
|
|
||||||
packet_payload_size: float = 0
|
packet_payload_size: float = 0
|
||||||
"""Size of the packet."""
|
"""Size of the packet."""
|
||||||
|
|
||||||
|
|||||||
@@ -81,18 +81,6 @@ class Application(IOSoftware):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def send(self, payload: Any, session_id: str, **kwargs) -> bool:
|
|
||||||
"""
|
|
||||||
Sends a payload to the SessionManager.
|
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
|
||||||
is generated should be implemented in subclasses.
|
|
||||||
|
|
||||||
:param payload: The payload to send.
|
|
||||||
:return: True if successful, False otherwise.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Receives a payload from the SessionManager.
|
Receives a payload from the SessionManager.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class DatabaseClient(Application):
|
|||||||
"""
|
"""
|
||||||
self.server_ip_address = server_ip_address
|
self.server_ip_address = server_ip_address
|
||||||
self.server_password = server_password
|
self.server_password = server_password
|
||||||
self.sys_log.info(f"Configured the {self.name} with {server_ip_address=}, {server_password=}.")
|
self.sys_log.info(f"{self.name}: Configured the {self.name} with {server_ip_address=}, {server_password=}.")
|
||||||
|
|
||||||
def connect(self) -> bool:
|
def connect(self) -> bool:
|
||||||
"""Connect to a Database Service."""
|
"""Connect to a Database Service."""
|
||||||
@@ -60,13 +60,25 @@ class DatabaseClient(Application):
|
|||||||
def _connect(
|
def _connect(
|
||||||
self, server_ip_address: IPv4Address, password: Optional[str] = None, is_reattempt: bool = False
|
self, server_ip_address: IPv4Address, password: Optional[str] = None, is_reattempt: bool = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Connects the DatabaseClient to the DatabaseServer.
|
||||||
|
|
||||||
|
:param: server_ip_address: IP address of the database server
|
||||||
|
:type: server_ip_address: IPv4Address
|
||||||
|
|
||||||
|
:param: password: Password used to connect to the database server. Optional.
|
||||||
|
:type: password: Optional[str]
|
||||||
|
|
||||||
|
:param: is_reattempt: True if the connect request has been reattempted. Default False
|
||||||
|
:type: is_reattempt: Optional[bool]
|
||||||
|
"""
|
||||||
if is_reattempt:
|
if is_reattempt:
|
||||||
if self.connected:
|
if self.connected:
|
||||||
self.sys_log.info(f"DatabaseClient connected to {server_ip_address} authorised")
|
self.sys_log.info(f"{self.name}: DatabaseClient connected to {server_ip_address} authorised")
|
||||||
self.server_ip_address = server_ip_address
|
self.server_ip_address = server_ip_address
|
||||||
return self.connected
|
return self.connected
|
||||||
else:
|
else:
|
||||||
self.sys_log.info(f"DatabaseClient connected to {server_ip_address} declined")
|
self.sys_log.info(f"{self.name}: DatabaseClient connected to {server_ip_address} declined")
|
||||||
return False
|
return False
|
||||||
payload = {"type": "connect_request", "password": password}
|
payload = {"type": "connect_request", "password": password}
|
||||||
software_manager: SoftwareManager = self.software_manager
|
software_manager: SoftwareManager = self.software_manager
|
||||||
@@ -83,15 +95,29 @@ class DatabaseClient(Application):
|
|||||||
payload={"type": "disconnect"}, dest_ip_address=self.server_ip_address, dest_port=self.port
|
payload={"type": "disconnect"}, dest_ip_address=self.server_ip_address, dest_port=self.port
|
||||||
)
|
)
|
||||||
|
|
||||||
self.sys_log.info(f"DatabaseClient disconnected from {self.server_ip_address}")
|
self.sys_log.info(f"{self.name}: DatabaseClient disconnected from {self.server_ip_address}")
|
||||||
self.server_ip_address = None
|
self.server_ip_address = None
|
||||||
self.connected = False
|
self.connected = False
|
||||||
|
|
||||||
def _query(self, sql: str, query_id: str, is_reattempt: bool = False) -> bool:
|
def _query(self, sql: str, query_id: str, is_reattempt: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Send a query to the connected database server.
|
||||||
|
|
||||||
|
:param: sql: SQL query to send to the database server.
|
||||||
|
:type: sql: str
|
||||||
|
|
||||||
|
:param: query_id: ID of the query, used as reference
|
||||||
|
:type: query_id: str
|
||||||
|
|
||||||
|
:param: is_reattempt: True if the query request has been reattempted. Default False
|
||||||
|
:type: is_reattempt: Optional[bool]
|
||||||
|
"""
|
||||||
if is_reattempt:
|
if is_reattempt:
|
||||||
success = self._query_success_tracker.get(query_id)
|
success = self._query_success_tracker.get(query_id)
|
||||||
if success:
|
if success:
|
||||||
|
self.sys_log.info(f"{self.name}: Query successful {sql}")
|
||||||
return True
|
return True
|
||||||
|
self.sys_log.info(f"{self.name}: Unable to run query {sql}")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
software_manager: SoftwareManager = self.software_manager
|
software_manager: SoftwareManager = self.software_manager
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from typing import Any, Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from primaite.simulator.network.protocols.http import HttpRequestMethod, HttpRequestPacket, HttpResponsePacket
|
||||||
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
from primaite.simulator.system.applications.application import Application
|
from primaite.simulator.system.applications.application import Application
|
||||||
|
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||||
|
|
||||||
|
|
||||||
class WebBrowser(Application):
|
class WebBrowser(Application):
|
||||||
@@ -11,12 +16,29 @@ class WebBrowser(Application):
|
|||||||
The application requests and loads web pages using its domain name and requesting IP addresses using DNS.
|
The application requests and loads web pages using its domain name and requesting IP addresses using DNS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
domain_name: str
|
domain_name_ip_address: Optional[IPv4Address] = None
|
||||||
"The domain name of the webpage."
|
|
||||||
domain_name_ip_address: Optional[IPv4Address]
|
|
||||||
"The IP address of the domain name for the webpage."
|
"The IP address of the domain name for the webpage."
|
||||||
history: Dict[str]
|
|
||||||
"A dict that stores all of the previous domain names."
|
latest_response: Optional[HttpResponsePacket] = None
|
||||||
|
"""Keeps track of the latest HTTP response."""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs["name"] = "WebBrowser"
|
||||||
|
kwargs["protocol"] = IPProtocol.TCP
|
||||||
|
# default for web is port 80
|
||||||
|
if kwargs.get("port") is None:
|
||||||
|
kwargs["port"] = Port.HTTP
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.run()
|
||||||
|
|
||||||
|
def describe_state(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Produce a dictionary describing the current state of the WebBrowser.
|
||||||
|
|
||||||
|
:return: A dictionary capturing the current state of the WebBrowser and its child objects.
|
||||||
|
"""
|
||||||
|
return super().describe_state()
|
||||||
|
|
||||||
def reset_component_for_episode(self, episode: int):
|
def reset_component_for_episode(self, episode: int):
|
||||||
"""
|
"""
|
||||||
@@ -25,30 +47,90 @@ class WebBrowser(Application):
|
|||||||
This method ensures the Application is ready for a new episode, including resetting any
|
This method ensures the Application is ready for a new episode, including resetting any
|
||||||
stateful properties or statistics, and clearing any message queues.
|
stateful properties or statistics, and clearing any message queues.
|
||||||
"""
|
"""
|
||||||
self.domain_name = ""
|
|
||||||
self.domain_name_ip_address = None
|
self.domain_name_ip_address = None
|
||||||
self.history = {}
|
self.latest_response = None
|
||||||
|
|
||||||
def send(self, payload: Any, session_id: str, **kwargs) -> bool:
|
def get_webpage(self, url: str) -> bool:
|
||||||
|
"""
|
||||||
|
Retrieve the webpage.
|
||||||
|
|
||||||
|
This should send a request to the web server which also requests for a list of users
|
||||||
|
|
||||||
|
:param: url: The address of the web page the browser requests
|
||||||
|
:type: url: str
|
||||||
|
"""
|
||||||
|
# reset latest response
|
||||||
|
self.latest_response = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
except Exception:
|
||||||
|
self.sys_log.error(f"{url} is not a valid URL")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# get the IP address of the domain name via DNS
|
||||||
|
dns_client: DNSClient = self.software_manager.software["DNSClient"]
|
||||||
|
|
||||||
|
domain_exists = dns_client.check_domain_exists(target_domain=parsed_url.hostname)
|
||||||
|
|
||||||
|
# if domain does not exist, the request fails
|
||||||
|
if domain_exists:
|
||||||
|
# set current domain name IP address
|
||||||
|
self.domain_name_ip_address = dns_client.dns_cache[parsed_url.hostname]
|
||||||
|
else:
|
||||||
|
# check if url is an ip address
|
||||||
|
try:
|
||||||
|
self.domain_name_ip_address = IPv4Address(parsed_url.hostname)
|
||||||
|
except Exception:
|
||||||
|
# unable to deal with this request
|
||||||
|
self.sys_log.error(f"{self.name}: Unable to resolve URL {url}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# create HTTPRequest payload
|
||||||
|
payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url=url)
|
||||||
|
|
||||||
|
# send request
|
||||||
|
return self.send(
|
||||||
|
payload=payload,
|
||||||
|
dest_ip_address=self.domain_name_ip_address,
|
||||||
|
dest_port=parsed_url.port if parsed_url.port else Port.HTTP,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(
|
||||||
|
self,
|
||||||
|
payload: HttpRequestPacket,
|
||||||
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
|
dest_port: Optional[Port] = Port.HTTP,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Sends a payload to the SessionManager.
|
Sends a payload to the SessionManager.
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
:param payload: The payload to be sent.
|
||||||
is generated should be implemented in subclasses.
|
:param dest_ip_address: The ip address of the payload destination.
|
||||||
|
:param dest_port: The port of the payload destination.
|
||||||
|
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||||
|
|
||||||
:param payload: The payload to send.
|
|
||||||
:return: True if successful, False otherwise.
|
:return: True if successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
pass
|
self.sys_log.info(f"{self.name}: Sending HTTP {payload.request_method.name} {payload.request_url}")
|
||||||
|
|
||||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
return super().send(
|
||||||
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def receive(self, payload: HttpResponsePacket, session_id: Optional[str] = None, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Receives a payload from the SessionManager.
|
Receives a payload from the SessionManager.
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
:param payload: The payload to be sent.
|
||||||
is generated should be implemented in subclasses.
|
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||||
|
|
||||||
:param payload: The payload to receive.
|
|
||||||
:return: True if successful, False otherwise.
|
:return: True if successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
pass
|
if not isinstance(payload, HttpResponsePacket):
|
||||||
|
self.sys_log.error(f"{self.name} received a packet that is not an HttpResponsePacket")
|
||||||
|
return False
|
||||||
|
self.sys_log.info(f"{self.name}: Received HTTP {payload.status_code.value}")
|
||||||
|
self.latest_response = payload
|
||||||
|
return True
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class SessionManager:
|
|||||||
self.sessions_by_key[session_key] = session
|
self.sessions_by_key[session_key] = session
|
||||||
self.sessions_by_uuid[session.uuid] = session
|
self.sessions_by_uuid[session.uuid] = session
|
||||||
|
|
||||||
outbound_nic.send_frame(frame)
|
return outbound_nic.send_frame(frame)
|
||||||
|
|
||||||
def receive_frame(self, frame: Frame):
|
def receive_frame(self, frame: Frame):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class SoftwareManager:
|
|||||||
dest_ip_address: Optional[IPv4Address] = None,
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
dest_port: Optional[Port] = None,
|
dest_port: Optional[Port] = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
):
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Send a payload to the SessionManager.
|
Send a payload to the SessionManager.
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ class SoftwareManager:
|
|||||||
:param dest_port: The port of the payload destination.
|
:param dest_port: The port of the payload destination.
|
||||||
:param session_id: The Session ID the payload is to originate from. Optional.
|
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||||
"""
|
"""
|
||||||
self.session_manager.receive_payload_from_software_manager(
|
return self.session_manager.receive_payload_from_software_manager(
|
||||||
payload=payload, dst_ip_address=dest_ip_address, dst_port=dest_port, session_id=session_id
|
payload=payload, dst_ip_address=dest_ip_address, dst_port=dest_port, session_id=session_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -155,10 +155,10 @@ class DatabaseService(Service):
|
|||||||
if self.password == password:
|
if self.password == password:
|
||||||
status_code = 200 # ok
|
status_code = 200 # ok
|
||||||
self.connections[session_id] = datetime.now()
|
self.connections[session_id] = datetime.now()
|
||||||
self.sys_log.info(f"Connect request for {session_id=} authorised")
|
self.sys_log.info(f"{self.name}: Connect request for {session_id=} authorised")
|
||||||
else:
|
else:
|
||||||
status_code = 401 # Unauthorised
|
status_code = 401 # Unauthorised
|
||||||
self.sys_log.info(f"Connect request for {session_id=} declined")
|
self.sys_log.info(f"{self.name}: Connect request for {session_id=} declined")
|
||||||
else:
|
else:
|
||||||
status_code = 404 # service not found
|
status_code = 404 # service not found
|
||||||
return {"status_code": status_code, "type": "connect_response", "response": status_code == 200}
|
return {"status_code": status_code, "type": "connect_response", "response": status_code == 200}
|
||||||
|
|||||||
@@ -72,19 +72,25 @@ class DNSClient(Service):
|
|||||||
:param: session_id: The Session ID the payload is to originate from. Optional.
|
:param: session_id: The Session ID the payload is to originate from. Optional.
|
||||||
:param: is_reattempt: Checks if the request has been reattempted. Default is False.
|
:param: is_reattempt: Checks if the request has been reattempted. Default is False.
|
||||||
"""
|
"""
|
||||||
|
# check if DNS server is configured
|
||||||
|
if self.dns_server is None:
|
||||||
|
self.sys_log.error(f"{self.name}: DNS Server is not configured")
|
||||||
|
return False
|
||||||
|
|
||||||
# check if the target domain is in the client's DNS cache
|
# check if the target domain is in the client's DNS cache
|
||||||
payload = DNSPacket(dns_request=DNSRequest(domain_name_request=target_domain))
|
payload = DNSPacket(dns_request=DNSRequest(domain_name_request=target_domain))
|
||||||
|
|
||||||
# check if the domain is already in the DNS cache
|
# check if the domain is already in the DNS cache
|
||||||
if target_domain in self.dns_cache:
|
if target_domain in self.dns_cache:
|
||||||
self.sys_log.info(
|
self.sys_log.info(
|
||||||
f"DNS Client: Domain lookup for {target_domain} successful, resolves to {self.dns_cache[target_domain]}"
|
f"{self.name}: Domain lookup for {target_domain} successful,"
|
||||||
|
f"resolves to {self.dns_cache[target_domain]}"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# return False if already reattempted
|
# return False if already reattempted
|
||||||
if is_reattempt:
|
if is_reattempt:
|
||||||
self.sys_log.info(f"DNS Client: Domain lookup for {target_domain} failed")
|
self.sys_log.info(f"{self.name}: Domain lookup for {target_domain} failed")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# send a request to check if domain name exists in the DNS Server
|
# send a request to check if domain name exists in the DNS Server
|
||||||
@@ -104,14 +110,13 @@ class DNSClient(Service):
|
|||||||
self,
|
self,
|
||||||
payload: DNSPacket,
|
payload: DNSPacket,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
|
dest_port: Optional[Port] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Sends a payload to the SessionManager.
|
Sends a payload to the SessionManager.
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
|
||||||
is generated should be implemented in subclasses.
|
|
||||||
|
|
||||||
:param payload: The payload to be sent.
|
:param payload: The payload to be sent.
|
||||||
:param dest_ip_address: The ip address of the payload destination.
|
:param dest_ip_address: The ip address of the payload destination.
|
||||||
:param dest_port: The port of the payload destination.
|
:param dest_port: The port of the payload destination.
|
||||||
@@ -119,10 +124,11 @@ class DNSClient(Service):
|
|||||||
|
|
||||||
:return: True if successful, False otherwise.
|
:return: True if successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
# create DNS request packet
|
self.sys_log.info(f"{self.name}: Sending DNS request to resolve {payload.dns_request.domain_name_request}")
|
||||||
software_manager: SoftwareManager = self.software_manager
|
|
||||||
software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
|
return super().send(
|
||||||
return True
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
def receive(
|
def receive(
|
||||||
self,
|
self,
|
||||||
@@ -133,9 +139,6 @@ class DNSClient(Service):
|
|||||||
"""
|
"""
|
||||||
Receives a payload from the SessionManager.
|
Receives a payload from the SessionManager.
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
|
||||||
is generated should be implemented in subclasses.
|
|
||||||
|
|
||||||
:param payload: The payload to be sent.
|
:param payload: The payload to be sent.
|
||||||
:param session_id: The Session ID the payload is to originate from. Optional.
|
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||||
:return: True if successful, False otherwise.
|
:return: True if successful, False otherwise.
|
||||||
@@ -144,12 +147,16 @@ class DNSClient(Service):
|
|||||||
if not isinstance(payload, DNSPacket):
|
if not isinstance(payload, DNSPacket):
|
||||||
_LOGGER.debug(f"{payload} is not a DNSPacket")
|
_LOGGER.debug(f"{payload} is not a DNSPacket")
|
||||||
return False
|
return False
|
||||||
# cast payload into a DNS packet
|
|
||||||
payload: DNSPacket = payload
|
|
||||||
if payload.dns_reply is not None:
|
if payload.dns_reply is not None:
|
||||||
# add the IP address to the client cache
|
# add the IP address to the client cache
|
||||||
if payload.dns_reply.domain_name_ip_address:
|
if payload.dns_reply.domain_name_ip_address:
|
||||||
|
self.sys_log.info(
|
||||||
|
f"{self.name}: Resolved domain name {payload.dns_request.domain_name_request} "
|
||||||
|
f"to {payload.dns_reply.domain_name_ip_address}"
|
||||||
|
)
|
||||||
self.dns_cache[payload.dns_request.domain_name_request] = payload.dns_reply.domain_name_ip_address
|
self.dns_cache[payload.dns_request.domain_name_request] = payload.dns_reply.domain_name_ip_address
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
self.sys_log.error(f"Failed to resolve domain name {payload.dns_request.domain_name_request}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -96,13 +96,13 @@ class DNSServer(Service):
|
|||||||
payload: DNSPacket = payload
|
payload: DNSPacket = payload
|
||||||
if payload.dns_request is not None:
|
if payload.dns_request is not None:
|
||||||
self.sys_log.info(
|
self.sys_log.info(
|
||||||
f"DNS Server: Received domain lookup request for {payload.dns_request.domain_name_request} "
|
f"{self.name}: Received domain lookup request for {payload.dns_request.domain_name_request} "
|
||||||
f"from session {session_id}"
|
f"from session {session_id}"
|
||||||
)
|
)
|
||||||
# generate a reply with the correct DNS IP address
|
# generate a reply with the correct DNS IP address
|
||||||
payload = payload.generate_reply(self.dns_lookup(payload.dns_request.domain_name_request))
|
payload = payload.generate_reply(self.dns_lookup(payload.dns_request.domain_name_request))
|
||||||
self.sys_log.info(
|
self.sys_log.info(
|
||||||
f"DNS Server: Responding to domain lookup request for {payload.dns_request.domain_name_request} "
|
f"{self.name}: Responding to domain lookup request for {payload.dns_request.domain_name_request} "
|
||||||
f"with ip address: {payload.dns_reply.domain_name_ip_address}"
|
f"with ip address: {payload.dns_reply.domain_name_ip_address}"
|
||||||
)
|
)
|
||||||
# send reply
|
# send reply
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ class FTPClient(FTPServiceABC):
|
|||||||
"""
|
"""
|
||||||
# if client service is down, return error
|
# if client service is down, return error
|
||||||
if self.operating_state != ServiceOperatingState.RUNNING:
|
if self.operating_state != ServiceOperatingState.RUNNING:
|
||||||
|
self.sys_log.error("FTP Client is not running")
|
||||||
payload.status_code = FTPStatusCode.ERROR
|
payload.status_code = FTPStatusCode.ERROR
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}")
|
||||||
|
|
||||||
# process client specific commands, otherwise call super
|
# process client specific commands, otherwise call super
|
||||||
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
|
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ class FTPClient(FTPServiceABC):
|
|||||||
self,
|
self,
|
||||||
dest_ip_address: Optional[IPv4Address] = None,
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
dest_port: Optional[Port] = Port.FTP,
|
dest_port: Optional[Port] = Port.FTP,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
is_reattempt: Optional[bool] = False,
|
is_reattempt: Optional[bool] = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -72,20 +76,30 @@ class FTPClient(FTPServiceABC):
|
|||||||
ftp_command=FTPCommand.PORT,
|
ftp_command=FTPCommand.PORT,
|
||||||
ftp_command_args=Port.FTP,
|
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:
|
if self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id):
|
||||||
return True
|
if payload.status_code == FTPStatusCode.OK:
|
||||||
else:
|
self.sys_log.info(
|
||||||
if is_reattempt:
|
f"{self.name}: Successfully connected to FTP Server "
|
||||||
# reattempt failed
|
f"{dest_ip_address} via port {payload.ftp_command_args.value}"
|
||||||
return False
|
)
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
# try again
|
if is_reattempt:
|
||||||
self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port, is_reattempt=True)
|
# reattempt failed
|
||||||
|
self.sys_log.info(
|
||||||
|
f"{self.name}: Unable to connect to FTP Server "
|
||||||
|
f"{dest_ip_address} via port {payload.ftp_command_args.value}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# try again
|
||||||
|
self._connect_to_server(
|
||||||
|
dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, is_reattempt=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.sys_log.error(f"{self.name}: Unable to send FTPPacket")
|
||||||
|
return False
|
||||||
|
|
||||||
def _disconnect_from_server(
|
def _disconnect_from_server(
|
||||||
self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP
|
self, dest_ip_address: Optional[IPv4Address] = None, dest_port: Optional[Port] = Port.FTP
|
||||||
@@ -119,6 +133,7 @@ class FTPClient(FTPServiceABC):
|
|||||||
dest_folder_name: str,
|
dest_folder_name: str,
|
||||||
dest_file_name: str,
|
dest_file_name: str,
|
||||||
dest_port: Optional[Port] = Port.FTP,
|
dest_port: Optional[Port] = Port.FTP,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
real_file_path: Optional[str] = None,
|
real_file_path: Optional[str] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -144,6 +159,9 @@ class FTPClient(FTPServiceABC):
|
|||||||
|
|
||||||
:param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP.
|
:param: dest_port: The open port of the machine that hosts the FTP Server. Default is Port.FTP.
|
||||||
:type: dest_port: Optional[Port]
|
:type: dest_port: Optional[Port]
|
||||||
|
|
||||||
|
:param: session_id: The id of the session
|
||||||
|
:type: session_id: Optional[str]
|
||||||
"""
|
"""
|
||||||
# check if the file to transfer exists on the client
|
# check if the file to transfer exists on the client
|
||||||
file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name)
|
file_to_transfer: File = self.file_system.get_file(folder_name=src_folder_name, file_name=src_file_name)
|
||||||
@@ -152,10 +170,7 @@ class FTPClient(FTPServiceABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# check if FTP is currently connected to IP
|
# check if FTP is currently connected to IP
|
||||||
self.connected = self._connect_to_server(
|
self.connected = self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port)
|
||||||
dest_ip_address=dest_ip_address,
|
|
||||||
dest_port=dest_port,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
return False
|
return False
|
||||||
@@ -206,10 +221,7 @@ class FTPClient(FTPServiceABC):
|
|||||||
:type: dest_port: Optional[Port]
|
:type: dest_port: Optional[Port]
|
||||||
"""
|
"""
|
||||||
# check if FTP is currently connected to IP
|
# check if FTP is currently connected to IP
|
||||||
self.connected = self._connect_to_server(
|
self.connected = self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port)
|
||||||
dest_ip_address=dest_ip_address,
|
|
||||||
dest_port=dest_port,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
return False
|
return False
|
||||||
@@ -232,10 +244,10 @@ class FTPClient(FTPServiceABC):
|
|||||||
|
|
||||||
# the payload should have ok status code
|
# the payload should have ok status code
|
||||||
if payload.status_code == FTPStatusCode.OK:
|
if payload.status_code == FTPStatusCode.OK:
|
||||||
self.sys_log.info(f"File {src_folder_name}/{src_file_name} found in FTP server.")
|
self.sys_log.info(f"{self.name}: File {src_folder_name}/{src_file_name} found in FTP server.")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.sys_log.error(f"File {src_folder_name}/{src_file_name} does not exist in FTP server")
|
self.sys_log.error(f"{self.name}: File {src_folder_name}/{src_file_name} does not exist in FTP server")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool:
|
def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool:
|
||||||
@@ -252,5 +264,14 @@ class FTPClient(FTPServiceABC):
|
|||||||
self.sys_log.error(f"{payload} is not an FTP packet")
|
self.sys_log.error(f"{payload} is not an FTP packet")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
Ignore ftp payload if status code is None.
|
||||||
|
|
||||||
|
This helps prevent an FTP request loop - FTP client and servers can exist on
|
||||||
|
the same node.
|
||||||
|
"""
|
||||||
|
if payload.status_code is None:
|
||||||
|
return False
|
||||||
|
|
||||||
self._process_ftp_command(payload=payload, session_id=session_id)
|
self._process_ftp_command(payload=payload, session_id=session_id)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from typing import Any, Dict, Optional
|
|||||||
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
||||||
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.session_manager import Session
|
|
||||||
from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC
|
from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC
|
||||||
from primaite.simulator.system.services.service import ServiceOperatingState
|
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||||
|
|
||||||
@@ -30,14 +29,6 @@ class FTPServer(FTPServiceABC):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.start()
|
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:
|
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket:
|
||||||
"""
|
"""
|
||||||
Process the command in the FTP Packet.
|
Process the command in the FTP Packet.
|
||||||
@@ -47,11 +38,16 @@ class FTPServer(FTPServiceABC):
|
|||||||
:param: session_id: session ID linked to the FTP Packet. Optional.
|
:param: session_id: session ID linked to the FTP Packet. Optional.
|
||||||
:type: session_id: Optional[str]
|
:type: session_id: Optional[str]
|
||||||
"""
|
"""
|
||||||
|
# error code by default
|
||||||
|
payload.status_code = FTPStatusCode.ERROR
|
||||||
|
|
||||||
# if server service is down, return error
|
# if server service is down, return error
|
||||||
if self.operating_state != ServiceOperatingState.RUNNING:
|
if self.operating_state != ServiceOperatingState.RUNNING:
|
||||||
payload.status_code = FTPStatusCode.ERROR
|
self.sys_log.error("FTP Server not running")
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}")
|
||||||
|
|
||||||
if session_id:
|
if session_id:
|
||||||
session_details = self._get_session_details(session_id)
|
session_details = self._get_session_details(session_id)
|
||||||
|
|
||||||
@@ -67,9 +63,13 @@ class FTPServer(FTPServiceABC):
|
|||||||
payload.status_code = FTPStatusCode.OK
|
payload.status_code = FTPStatusCode.OK
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
self.sys_log.error(f"Invalid Port {payload.ftp_command_args}")
|
||||||
|
return payload
|
||||||
|
|
||||||
if payload.ftp_command == FTPCommand.QUIT:
|
if payload.ftp_command == FTPCommand.QUIT:
|
||||||
self.connections.pop(session_id)
|
self.connections.pop(session_id)
|
||||||
payload.status_code = FTPStatusCode.OK
|
payload.status_code = FTPStatusCode.OK
|
||||||
|
return payload
|
||||||
|
|
||||||
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
|
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
|
||||||
|
|
||||||
@@ -79,5 +79,15 @@ class FTPServer(FTPServiceABC):
|
|||||||
self.sys_log.error(f"{payload} is not an FTP packet")
|
self.sys_log.error(f"{payload} is not an FTP packet")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
Ignore ftp payload if status code is defined.
|
||||||
|
|
||||||
|
This means that an FTP server has already handled the packet and
|
||||||
|
prevents an FTP request loop - FTP client and servers can exist on
|
||||||
|
the same node.
|
||||||
|
"""
|
||||||
|
if payload.status_code is not None:
|
||||||
|
return False
|
||||||
|
|
||||||
self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id)
|
self.send(self._process_ftp_command(payload=payload, session_id=session_id), session_id)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from typing import Optional
|
|||||||
from primaite.simulator.file_system.file_system import File
|
from primaite.simulator.file_system.file_system import File
|
||||||
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
||||||
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.services.service import Service
|
from primaite.simulator.system.services.service import Service
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ class FTPServiceABC(Service, ABC):
|
|||||||
file_name=file_name, folder_name=folder_name, size=file_size, real=is_real
|
file_name=file_name, folder_name=folder_name, size=file_size, real=is_real
|
||||||
)
|
)
|
||||||
self.sys_log.info(
|
self.sys_log.info(
|
||||||
f"Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/"
|
f"{self.name}: Created item in {self.sys_log.hostname}: {payload.ftp_command_args['dest_folder_name']}/"
|
||||||
f"{payload.ftp_command_args['dest_file_name']}"
|
f"{payload.ftp_command_args['dest_file_name']}"
|
||||||
)
|
)
|
||||||
if is_real:
|
if is_real:
|
||||||
@@ -77,6 +76,7 @@ class FTPServiceABC(Service, ABC):
|
|||||||
dest_ip_address: Optional[IPv4Address] = None,
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
dest_port: Optional[Port] = None,
|
dest_port: Optional[Port] = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
|
is_response: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Sends data from the host FTP Service's machine to another FTP Service's host machine.
|
Sends data from the host FTP Service's machine to another FTP Service's host machine.
|
||||||
@@ -98,6 +98,9 @@ class FTPServiceABC(Service, ABC):
|
|||||||
|
|
||||||
:param: session_id: session ID linked to the FTP Packet. Optional.
|
:param: session_id: session ID linked to the FTP Packet. Optional.
|
||||||
:type: session_id: Optional[str]
|
:type: session_id: Optional[str]
|
||||||
|
|
||||||
|
:param: is_response: is true if the data being sent is in response to a request. Default False.
|
||||||
|
:type: is_response: bool
|
||||||
"""
|
"""
|
||||||
# send STOR request
|
# send STOR request
|
||||||
payload: FTPPacket = FTPPacket(
|
payload: FTPPacket = FTPPacket(
|
||||||
@@ -109,13 +112,14 @@ class FTPServiceABC(Service, ABC):
|
|||||||
"real_file_path": file.sim_path if file.real else None,
|
"real_file_path": file.sim_path if file.real else None,
|
||||||
},
|
},
|
||||||
packet_payload_size=file.sim_size,
|
packet_payload_size=file.sim_size,
|
||||||
|
status_code=FTPStatusCode.OK if is_response else None,
|
||||||
)
|
)
|
||||||
software_manager: SoftwareManager = self.software_manager
|
self.sys_log.info(f"{self.name}: Sending file {file.folder.name}/{file.name}")
|
||||||
software_manager.send_payload_to_session_manager(
|
response = self.send(
|
||||||
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if payload.status_code == FTPStatusCode.OK:
|
if response and payload.status_code == FTPStatusCode.OK:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -149,7 +153,32 @@ class FTPServiceABC(Service, ABC):
|
|||||||
dest_file_name=dest_file_name,
|
dest_file_name=dest_file_name,
|
||||||
dest_folder_name=dest_folder_name,
|
dest_folder_name=dest_folder_name,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
|
is_response=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}")
|
self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def send(
|
||||||
|
self,
|
||||||
|
payload: FTPPacket,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
|
dest_port: Optional[Port] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Sends a payload to the SessionManager.
|
||||||
|
|
||||||
|
:param payload: The payload to be sent.
|
||||||
|
:param dest_ip_address: The ip address of the payload destination.
|
||||||
|
:param dest_port: The port of the payload destination.
|
||||||
|
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||||
|
|
||||||
|
:return: True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
self.sys_log.info(f"{self.name}: Sending FTP {payload.ftp_command.name} {payload.ftp_command_args}")
|
||||||
|
|
||||||
|
return super().send(
|
||||||
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs
|
||||||
|
)
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ class DataManipulationBot(DatabaseClient):
|
|||||||
self.server_ip_address = server_ip_address
|
self.server_ip_address = server_ip_address
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.server_password = server_password
|
self.server_password = server_password
|
||||||
self.sys_log.info(f"Configured the {self.name} with {server_ip_address=}, {payload=}, {server_password=}.")
|
self.sys_log.info(
|
||||||
|
f"{self.name}: Configured the {self.name} with {server_ip_address=}, {payload=}, {server_password=}."
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run the DataManipulationBot."""
|
"""Run the DataManipulationBot."""
|
||||||
if self.server_ip_address and self.payload:
|
if self.server_ip_address and self.payload:
|
||||||
self.sys_log.info(f"Attempting to start the {self.name}")
|
self.sys_log.info(f"{self.name}: Attempting to start the {self.name}")
|
||||||
super().run()
|
super().run()
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
self.connect()
|
self.connect()
|
||||||
@@ -46,4 +48,4 @@ class DataManipulationBot(DatabaseClient):
|
|||||||
self.query(self.payload)
|
self.query(self.payload)
|
||||||
self.sys_log.info(f"{self.name} payload delivered: {self.payload}")
|
self.sys_log.info(f"{self.name} payload delivered: {self.payload}")
|
||||||
else:
|
else:
|
||||||
self.sys_log.error(f"Failed to start the {self.name} as it requires both a target_io_address and payload.")
|
self.sys_log.error(f"Failed to start the {self.name} as it requires both a target_ip_address and payload.")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from primaite import getLogger
|
from primaite import getLogger
|
||||||
from primaite.simulator.core import RequestManager, RequestType
|
from primaite.simulator.core import RequestManager, RequestType
|
||||||
@@ -72,45 +72,6 @@ class Service(IOSoftware):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def send(
|
|
||||||
self,
|
|
||||||
payload: Any,
|
|
||||||
session_id: Optional[str] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Sends a payload to the SessionManager.
|
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
|
||||||
is generated should be implemented in subclasses.
|
|
||||||
|
|
||||||
:param: payload: The payload to send.
|
|
||||||
:param: session_id: The id of the session
|
|
||||||
|
|
||||||
:return: True if successful, False otherwise.
|
|
||||||
"""
|
|
||||||
self.software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
|
|
||||||
|
|
||||||
def receive(
|
|
||||||
self,
|
|
||||||
payload: Any,
|
|
||||||
session_id: Optional[str] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Receives a payload from the SessionManager.
|
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
|
||||||
is generated should be implemented in subclasses.
|
|
||||||
|
|
||||||
:param: payload: The payload to send.
|
|
||||||
:param: session_id: The id of the session
|
|
||||||
|
|
||||||
:return: True if successful, False otherwise.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""Stop the service."""
|
"""Stop the service."""
|
||||||
if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]:
|
if self.operating_state in [ServiceOperatingState.RUNNING, ServiceOperatingState.PAUSED]:
|
||||||
|
|||||||
145
src/primaite/simulator/system/services/web_server/web_server.py
Normal file
145
src/primaite/simulator/system/services/web_server/web_server.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
from ipaddress import IPv4Address
|
||||||
|
from typing import Any, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from primaite.simulator.network.protocols.http import (
|
||||||
|
HttpRequestMethod,
|
||||||
|
HttpRequestPacket,
|
||||||
|
HttpResponsePacket,
|
||||||
|
HttpStatusCode,
|
||||||
|
)
|
||||||
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
|
from primaite.simulator.system.applications.database_client import DatabaseClient
|
||||||
|
from primaite.simulator.system.services.service import Service
|
||||||
|
|
||||||
|
|
||||||
|
class WebServer(Service):
|
||||||
|
"""Class used to represent a Web Server Service in simulation."""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs["name"] = "WebServer"
|
||||||
|
kwargs["protocol"] = IPProtocol.TCP
|
||||||
|
# default for web is port 80
|
||||||
|
if kwargs.get("port") is None:
|
||||||
|
kwargs["port"] = Port.HTTP
|
||||||
|
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._install_web_files()
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def _install_web_files(self):
|
||||||
|
"""
|
||||||
|
Installs the files hosted by the web service.
|
||||||
|
|
||||||
|
This is usually HTML, CSS, JS or PHP files requested by browsers to display the webpage.
|
||||||
|
"""
|
||||||
|
# index HTML main file
|
||||||
|
self.file_system.create_file(file_name="index.html", folder_name="primaite")
|
||||||
|
|
||||||
|
def _process_http_request(self, payload: HttpRequestPacket, session_id: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Parse the HttpRequestPacket.
|
||||||
|
|
||||||
|
:param: payload: Payload containing th HttpRequestPacket
|
||||||
|
:type: payload: HttpRequestPacket
|
||||||
|
|
||||||
|
:param: session_id: Session id of the http request
|
||||||
|
:type: session_id: Optional[str]
|
||||||
|
"""
|
||||||
|
response = HttpResponsePacket()
|
||||||
|
|
||||||
|
self.sys_log.info(f"{self.name}: Received HTTP {payload.request_method.name} {payload.request_url}")
|
||||||
|
|
||||||
|
# check the type of HTTP request
|
||||||
|
if payload.request_method == HttpRequestMethod.GET:
|
||||||
|
response = self._handle_get_request(payload=payload)
|
||||||
|
|
||||||
|
elif payload.request_method == HttpRequestMethod.POST:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
# send a method not allowed response
|
||||||
|
response.status_code = HttpStatusCode.METHOD_NOT_ALLOWED
|
||||||
|
|
||||||
|
# send response to web client
|
||||||
|
self.send(payload=response, session_id=session_id)
|
||||||
|
|
||||||
|
# return true if response is OK
|
||||||
|
return response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
|
def _handle_get_request(self, payload: HttpRequestPacket) -> HttpResponsePacket:
|
||||||
|
"""
|
||||||
|
Handle a GET HTTP request.
|
||||||
|
|
||||||
|
:param: payload: HTTP request payload
|
||||||
|
:type: payload: HttpRequestPacket
|
||||||
|
"""
|
||||||
|
response = HttpResponsePacket(status_code=HttpStatusCode.NOT_FOUND, payload=payload)
|
||||||
|
try:
|
||||||
|
parsed_url = urlparse(payload.request_url)
|
||||||
|
path = parsed_url.path.strip("/")
|
||||||
|
|
||||||
|
if len(path) < 1:
|
||||||
|
# query succeeded
|
||||||
|
response.status_code = HttpStatusCode.OK
|
||||||
|
|
||||||
|
if path.startswith("users"):
|
||||||
|
# get data from DatabaseServer
|
||||||
|
db_client: DatabaseClient = self.software_manager.software["DatabaseClient"]
|
||||||
|
# get all users
|
||||||
|
if db_client.query("SELECT * FROM user;"):
|
||||||
|
# query succeeded
|
||||||
|
response.status_code = HttpStatusCode.OK
|
||||||
|
|
||||||
|
return response
|
||||||
|
except Exception:
|
||||||
|
# something went wrong on the server
|
||||||
|
response.status_code = HttpStatusCode.INTERNAL_SERVER_ERROR
|
||||||
|
return response
|
||||||
|
|
||||||
|
def send(
|
||||||
|
self,
|
||||||
|
payload: HttpResponsePacket,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
|
dest_port: Optional[Port] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Sends a payload to the SessionManager.
|
||||||
|
|
||||||
|
The specifics of how the payload is processed and whether a response payload
|
||||||
|
is generated should be implemented in subclasses.
|
||||||
|
|
||||||
|
:param: payload: The payload to send.
|
||||||
|
:param: session_id: The id of the session
|
||||||
|
:param dest_ip_address: The ip address of the payload destination.
|
||||||
|
:param dest_port: The port of the payload destination.
|
||||||
|
|
||||||
|
:return: True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
self.sys_log.info(f"{self.name}: Sending HTTP Response {payload.status_code}")
|
||||||
|
|
||||||
|
return super().send(
|
||||||
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def receive(
|
||||||
|
self,
|
||||||
|
payload: Any,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Receives a payload from the SessionManager.
|
||||||
|
|
||||||
|
:param: payload: The payload to send.
|
||||||
|
:param: session_id: The id of the session. Optional.
|
||||||
|
"""
|
||||||
|
# check if the payload is an HTTPPacket
|
||||||
|
if not isinstance(payload, HttpRequestPacket):
|
||||||
|
self.sys_log.error("Payload is not an HTTPPacket")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._process_http_request(payload=payload, session_id=session_id)
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from ipaddress import IPv4Address
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
from primaite.simulator.core import RequestManager, RequestType, SimComponent
|
||||||
from primaite.simulator.file_system.file_system import FileSystem, Folder
|
from primaite.simulator.file_system.file_system import FileSystem, Folder
|
||||||
from primaite.simulator.network.transmission.transport_layer import Port
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
|
from primaite.simulator.system.core.session_manager import Session
|
||||||
from primaite.simulator.system.core.sys_log import SysLog
|
from primaite.simulator.system.core.sys_log import SysLog
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +98,14 @@ class Software(SimComponent):
|
|||||||
am.add_request("scan", RequestType(func=lambda request, context: self.scan()))
|
am.add_request("scan", RequestType(func=lambda request, context: self.scan()))
|
||||||
return am
|
return am
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def describe_state(self) -> Dict:
|
def describe_state(self) -> Dict:
|
||||||
"""
|
"""
|
||||||
@@ -209,18 +219,27 @@ class IOSoftware(Software):
|
|||||||
)
|
)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def send(self, payload: Any, session_id: str, **kwargs) -> bool:
|
def send(
|
||||||
|
self,
|
||||||
|
payload: Any,
|
||||||
|
session_id: Optional[str] = None,
|
||||||
|
dest_ip_address: Optional[IPv4Address] = None,
|
||||||
|
dest_port: Optional[Port] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Sends a payload to the SessionManager.
|
Sends a payload to the SessionManager.
|
||||||
|
|
||||||
The specifics of how the payload is processed and whether a response payload
|
:param payload: The payload to be sent.
|
||||||
is generated should be implemented in subclasses.
|
:param dest_ip_address: The ip address of the payload destination.
|
||||||
|
:param dest_port: The port of the payload destination.
|
||||||
|
:param session_id: The Session ID the payload is to originate from. Optional.
|
||||||
|
|
||||||
:param payload: The payload to send.
|
:return: True if successful, False otherwise.
|
||||||
:param session_id: The identifier of the session that the payload is associated with.
|
|
||||||
:param kwargs: Additional keyword arguments specific to the implementation.
|
|
||||||
:return: True if the payload was successfully sent, False otherwise.
|
|
||||||
"""
|
"""
|
||||||
|
return self.software_manager.send_payload_to_session_manager(
|
||||||
|
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id
|
||||||
|
)
|
||||||
|
|
||||||
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
def receive(self, payload: Any, session_id: str, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
52
tests/integration_tests/system/test_web_client_server.py
Normal file
52
tests/integration_tests/system/test_web_client_server.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
|
from primaite.simulator.network.protocols.http import HttpStatusCode
|
||||||
|
from primaite.simulator.system.applications.application import ApplicationOperatingState
|
||||||
|
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||||
|
from primaite.simulator.system.services.service import ServiceOperatingState
|
||||||
|
|
||||||
|
|
||||||
|
def test_web_page_home_page(uc2_network):
|
||||||
|
"""Test to see if the browser is able to open the main page of the web server."""
|
||||||
|
client_1: Computer = uc2_network.get_node_by_hostname("client_1")
|
||||||
|
web_client: WebBrowser = client_1.software_manager.software["WebBrowser"]
|
||||||
|
web_client.run()
|
||||||
|
assert web_client.operating_state == ApplicationOperatingState.RUNNING
|
||||||
|
|
||||||
|
assert web_client.get_webpage("http://arcd.com/") is True
|
||||||
|
|
||||||
|
# latest reponse should have status code 200
|
||||||
|
assert web_client.latest_response is not None
|
||||||
|
assert web_client.latest_response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
|
|
||||||
|
def test_web_page_get_users_page_request_with_domain_name(uc2_network):
|
||||||
|
"""Test to see if the client can handle requests with domain names"""
|
||||||
|
client_1: Computer = uc2_network.get_node_by_hostname("client_1")
|
||||||
|
web_client: WebBrowser = client_1.software_manager.software["WebBrowser"]
|
||||||
|
web_client.run()
|
||||||
|
assert web_client.operating_state == ApplicationOperatingState.RUNNING
|
||||||
|
|
||||||
|
assert web_client.get_webpage("http://arcd.com/users/") is True
|
||||||
|
|
||||||
|
# latest reponse should have status code 200
|
||||||
|
assert web_client.latest_response is not None
|
||||||
|
assert web_client.latest_response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
|
|
||||||
|
def test_web_page_get_users_page_request_with_ip_address(uc2_network):
|
||||||
|
"""Test to see if the client can handle requests that use ip_address."""
|
||||||
|
client_1: Computer = uc2_network.get_node_by_hostname("client_1")
|
||||||
|
web_client: WebBrowser = client_1.software_manager.software["WebBrowser"]
|
||||||
|
web_client.run()
|
||||||
|
|
||||||
|
web_server: Server = uc2_network.get_node_by_hostname("web_server")
|
||||||
|
web_server_ip = web_server.nics.get(next(iter(web_server.nics))).ip_address
|
||||||
|
|
||||||
|
assert web_client.operating_state == ApplicationOperatingState.RUNNING
|
||||||
|
|
||||||
|
assert web_client.get_webpage(f"http://{web_server_ip}/users/") is True
|
||||||
|
|
||||||
|
# latest reponse should have status code 200
|
||||||
|
assert web_client.latest_response is not None
|
||||||
|
assert web_client.latest_response.status_code == HttpStatusCode.OK
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from primaite.simulator.network.hardware.nodes.computer import Computer
|
||||||
|
from primaite.simulator.network.protocols.http import HttpResponsePacket, HttpStatusCode
|
||||||
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
|
from primaite.simulator.system.applications.web_browser import WebBrowser
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def web_client() -> Computer:
|
||||||
|
node = Computer(
|
||||||
|
hostname="web_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
||||||
|
)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_web_client(web_client):
|
||||||
|
assert web_client is not None
|
||||||
|
web_browser: WebBrowser = web_client.software_manager.software["WebBrowser"]
|
||||||
|
assert web_browser.name is "WebBrowser"
|
||||||
|
assert web_browser.port is Port.HTTP
|
||||||
|
assert web_browser.protocol is IPProtocol.TCP
|
||||||
|
|
||||||
|
|
||||||
|
def test_receive_invalid_payload(web_client):
|
||||||
|
web_browser: WebBrowser = web_client.software_manager.software["WebBrowser"]
|
||||||
|
|
||||||
|
assert web_browser.receive(payload={}) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_receive_payload(web_client):
|
||||||
|
payload = HttpResponsePacket(status_code=HttpStatusCode.OK)
|
||||||
|
web_browser: WebBrowser = web_client.software_manager.software["WebBrowser"]
|
||||||
|
assert web_browser.latest_response is None
|
||||||
|
|
||||||
|
web_browser.receive(payload=payload)
|
||||||
|
|
||||||
|
assert web_browser.latest_response is not None
|
||||||
@@ -25,7 +25,11 @@ def dns_server() -> Node:
|
|||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def dns_client() -> Node:
|
def dns_client() -> Node:
|
||||||
node = Computer(
|
node = Computer(
|
||||||
hostname="dns_client", ip_address="192.168.1.11", subnet_mask="255.255.255.0", default_gateway="192.168.1.1"
|
hostname="dns_client",
|
||||||
|
ip_address="192.168.1.11",
|
||||||
|
subnet_mask="255.255.255.0",
|
||||||
|
default_gateway="192.168.1.1",
|
||||||
|
dns_server=IPv4Address("192.168.1.10"),
|
||||||
)
|
)
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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.computer import Computer
|
||||||
from primaite.simulator.network.hardware.nodes.server import Server
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket
|
from primaite.simulator.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
|
||||||
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.ftp.ftp_client import FTPClient
|
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
|
||||||
@@ -78,6 +78,7 @@ def test_ftp_client_store_file(ftp_client):
|
|||||||
"file_size": 24,
|
"file_size": 24,
|
||||||
},
|
},
|
||||||
packet_payload_size=24,
|
packet_payload_size=24,
|
||||||
|
status_code=FTPStatusCode.OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"]
|
ftp_client_service: FTPClient = ftp_client.software_manager.software["FTPClient"]
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from primaite.simulator.network.hardware.nodes.server import Server
|
||||||
|
from primaite.simulator.network.protocols.http import (
|
||||||
|
HttpRequestMethod,
|
||||||
|
HttpRequestPacket,
|
||||||
|
HttpResponsePacket,
|
||||||
|
HttpStatusCode,
|
||||||
|
)
|
||||||
|
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||||
|
from primaite.simulator.network.transmission.transport_layer import Port
|
||||||
|
from primaite.simulator.system.services.web_server.web_server import WebServer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def web_server() -> Server:
|
||||||
|
node = Server(
|
||||||
|
hostname="web_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=WebServer)
|
||||||
|
node.software_manager.software["WebServer"].start()
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_web_server(web_server):
|
||||||
|
assert web_server is not None
|
||||||
|
web_server_service: WebServer = web_server.software_manager.software["WebServer"]
|
||||||
|
assert web_server_service.name is "WebServer"
|
||||||
|
assert web_server_service.port is Port.HTTP
|
||||||
|
assert web_server_service.protocol is IPProtocol.TCP
|
||||||
|
|
||||||
|
|
||||||
|
def test_handling_get_request_not_found_path(web_server):
|
||||||
|
payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/fake-path")
|
||||||
|
|
||||||
|
web_server_service: WebServer = web_server.software_manager.software["WebServer"]
|
||||||
|
|
||||||
|
response: HttpResponsePacket = web_server_service._handle_get_request(payload=payload)
|
||||||
|
assert response.status_code == HttpStatusCode.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
def test_handling_get_request_home_page(web_server):
|
||||||
|
payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/")
|
||||||
|
|
||||||
|
web_server_service: WebServer = web_server.software_manager.software["WebServer"]
|
||||||
|
|
||||||
|
response: HttpResponsePacket = web_server_service._handle_get_request(payload=payload)
|
||||||
|
assert response.status_code == HttpStatusCode.OK
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_http_request_get(web_server):
|
||||||
|
payload = HttpRequestPacket(request_method=HttpRequestMethod.GET, request_url="http://domain.com/")
|
||||||
|
|
||||||
|
web_server_service: WebServer = web_server.software_manager.software["WebServer"]
|
||||||
|
|
||||||
|
assert web_server_service._process_http_request(payload=payload) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_http_request_method_not_allowed(web_server):
|
||||||
|
payload = HttpRequestPacket(request_method=HttpRequestMethod.DELETE, request_url="http://domain.com/")
|
||||||
|
|
||||||
|
web_server_service: WebServer = web_server.software_manager.software["WebServer"]
|
||||||
|
|
||||||
|
assert web_server_service._process_http_request(payload=payload) is False
|
||||||
Reference in New Issue
Block a user