#1943: web server + client + tests + a few improvements to syslogging

This commit is contained in:
Czar.Echavez
2023-10-03 14:59:48 +01:00
parent 161d441ad6
commit 4b5a73bd32
24 changed files with 536 additions and 112 deletions

View File

@@ -714,7 +714,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
@@ -937,6 +939,12 @@ class Node(SimComponent):
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 _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.

View File

@@ -1,4 +1,8 @@
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.ftp.ftp_client import FTPClient
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
class Computer(Node): class Computer(Node):
@@ -36,3 +40,18 @@ 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)
self.software_manager.install(FTPServer)
# Web Browser
self.software_manager.install(WebBrowser)
super()._install_system_software()

View File

@@ -596,6 +596,8 @@ class Router(Node):
self.arp.nics = self.nics self.arp.nics = self.nics
self.icmp.arp = self.arp self.icmp.arp = self.arp
self._install_system_software()
def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]: def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]:
""" """
Retrieve the port number for a given NIC. Retrieve the port number for a given NIC.

View File

@@ -34,6 +34,8 @@ class Switch(Node):
port.parent = self port.parent = self
port.port_num = port_num port.port_num = port_num
self._install_system_software()
def show(self, markdown: bool = False): def show(self, markdown: bool = False):
""" """
Prints a table of the SwitchPorts on the Switch. Prints a table of the SwitchPorts on the Switch.

View File

@@ -13,8 +13,8 @@ from primaite.simulator.system.services.database.database_service import Databas
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.dns.dns_server import DNSServer from primaite.simulator.system.services.dns.dns_server import DNSServer
from primaite.simulator.system.services.ftp.ftp_client import FTPClient from primaite.simulator.system.services.ftp.ftp_client import FTPClient
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot
from primaite.simulator.system.services.web_server.web_server_service import WebServerService
def client_server_routed() -> Network: def client_server_routed() -> Network:
@@ -260,6 +260,8 @@ def arcd_uc2_network() -> Network:
database_client.run() database_client.run()
database_client.connect() database_client.connect()
web_server.software_manager.install(WebServerService)
# 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)
@@ -275,8 +277,6 @@ 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",
@@ -305,4 +305,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

View File

@@ -0,0 +1,61 @@
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."""
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."""

View File

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

View File

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

View File

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

View File

@@ -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: 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.
"""
pass
def reset_component_for_episode(self, episode: int): def reset_component_for_episode(self, episode: int):
""" """
@@ -25,30 +47,84 @@ 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 not domain_exists:
return False
# set current domain name IP address
self.domain_name_ip_address = dns_client.dns_cache[parsed_url.hostname]
# 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

View File

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

View File

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

View File

@@ -73,10 +73,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}

View File

@@ -78,13 +78,14 @@ class DNSClient(Service):
# 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 +105,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 +119,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 +134,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 +142,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

View File

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

View File

@@ -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,27 @@ 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 self.send(payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id):
if payload.status_code == FTPStatusCode.OK: if payload.status_code == FTPStatusCode.OK:
self.sys_log.info(
f"{self.name}: Successfully connected to FTP Server "
f"{dest_ip_address} via port {payload.ftp_command_args.value}"
)
return True return True
else: else:
if is_reattempt: if is_reattempt:
# reattempt failed # 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 return False
else: else:
# try again # try again
self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port, is_reattempt=True) self._connect_to_server(
dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, is_reattempt=True
)
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 +130,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,
) -> bool: ) -> bool:
""" """
Send a file to a target IP address. Send a file to a target IP address.
@@ -143,6 +155,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)
@@ -151,10 +166,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
@@ -166,6 +178,7 @@ class FTPClient(FTPServiceABC):
dest_file_name=dest_file_name, dest_file_name=dest_file_name,
dest_ip_address=dest_ip_address, dest_ip_address=dest_ip_address,
dest_port=dest_port, dest_port=dest_port,
session_id=session_id,
) )
# send disconnect # send disconnect
@@ -204,10 +217,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,12 +242,36 @@ 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 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
)
def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool: def receive(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> bool:
""" """
Receives a payload from the SessionManager. Receives a payload from the SessionManager.

View File

@@ -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.
@@ -50,8 +41,11 @@ class FTPServer(FTPServiceABC):
# 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 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)

View File

@@ -5,7 +5,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
@@ -54,7 +53,7 @@ class FTPServiceABC(Service, ABC):
size=file_size, size=file_size,
) )
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']}"
) )
# file should exist # file should exist
@@ -103,12 +102,12 @@ class FTPServiceABC(Service, ABC):
}, },
packet_payload_size=file.sim_size, packet_payload_size=file.sim_size,
) )
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
@@ -146,3 +145,27 @@ class FTPServiceABC(Service, ABC):
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
)

View File

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

View File

@@ -1,8 +1,10 @@
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 import getLogger from primaite import getLogger
from primaite.simulator.core import Action, ActionManager from primaite.simulator.core import Action, ActionManager
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.software import IOSoftware from primaite.simulator.system.software import IOSoftware
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -76,20 +78,23 @@ class Service(IOSoftware):
self, self,
payload: Any, payload: Any,
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 :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: payload: The payload to send. :param session_id: The Session ID the payload is to originate from. Optional.
:param: session_id: The id of the session
:return: True if successful, False otherwise. :return: True if successful, False otherwise.
""" """
self.software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id) return super().send(
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs
)
def receive( def receive(
self, self,

View File

@@ -0,0 +1,136 @@
from ipaddress import IPv4Address
from typing import Any, Optional
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 WebServerService(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", real=True)
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.BAD_REQUEST, payload=payload)
try:
# 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)

View File

@@ -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 Action, ActionManager, SimComponent from primaite.simulator.core import Action, ActionManager, 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_action("scan", Action(func=lambda request, context: self.scan())) am.add_action("scan", Action(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:
""" """

View File

@@ -0,0 +1,19 @@
from primaite.simulator.network.hardware.nodes.computer import Computer
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_get_request(uc2_network):
"""Test to see if the client retrieves the correct web files."""
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/index.html") 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