#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
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
# Matched ARP request
@@ -937,6 +939,12 @@ class Node(SimComponent):
self.arp.nics = self.nics
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:
"""
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.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):
@@ -36,3 +40,18 @@ class Computer(Node):
def __init__(self, **kwargs):
super().__init__(**kwargs)
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.icmp.arp = self.arp
self._install_system_software()
def _get_port_of_nic(self, target_nic: NIC) -> Optional[int]:
"""
Retrieve the port number for a given NIC.

View File

@@ -34,6 +34,8 @@ class Switch(Node):
port.parent = self
port.port_num = port_num
self._install_system_software()
def show(self, markdown: bool = False):
"""
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_server import DNSServer
from primaite.simulator.system.services.ftp.ftp_client import FTPClient
from primaite.simulator.system.services.ftp.ftp_server import FTPServer
from primaite.simulator.system.services.red_services.data_manipulation_bot import DataManipulationBot
from primaite.simulator.system.services.web_server.web_server_service import WebServerService
def client_server_routed() -> Network:
@@ -260,6 +260,8 @@ def arcd_uc2_network() -> Network:
database_client.run()
database_client.connect()
web_server.software_manager.install(WebServerService)
# register the web_server to a domain
dns_server_service: DNSServer = domain_controller.software_manager.software["DNSServer"] # noqa
dns_server_service.dns_register("arcd.com", web_server.ip_address)
@@ -275,8 +277,6 @@ def arcd_uc2_network() -> Network:
backup_server.power_on()
network.connect(endpoint_b=backup_server.ethernet_port[1], endpoint_a=switch_1.switch_ports[4])
backup_server.software_manager.install(FTPServer)
# Security Suite
security_suite = Server(
hostname="security_suite",
@@ -305,4 +305,7 @@ def arcd_uc2_network() -> Network:
# Allow FTP requests
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

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
class DataPacket(BaseModel):
"""Data packet abstract class."""
payload: Any = None
"""Payload content of the packet."""
packet_payload_size: float = 0
"""Size of the packet."""

View File

@@ -81,18 +81,6 @@ class Application(IOSoftware):
"""
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:
"""
Receives a payload from the SessionManager.

View File

@@ -49,7 +49,7 @@ class DatabaseClient(Application):
"""
self.server_ip_address = server_ip_address
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:
"""Connect to a Database Service."""
@@ -60,13 +60,25 @@ class DatabaseClient(Application):
def _connect(
self, server_ip_address: IPv4Address, password: Optional[str] = None, is_reattempt: bool = False
) -> 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 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
return self.connected
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
payload = {"type": "connect_request", "password": password}
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
)
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.connected = False
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:
success = self._query_success_tracker.get(query_id)
if success:
self.sys_log.info(f"{self.name}: Query successful {sql}")
return True
self.sys_log.info(f"{self.name}: Unable to run query {sql}")
return False
else:
software_manager: SoftwareManager = self.software_manager

View File

@@ -1,7 +1,12 @@
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.services.dns.dns_client import DNSClient
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.
"""
domain_name: str
"The domain name of the webpage."
domain_name_ip_address: Optional[IPv4Address]
domain_name_ip_address: Optional[IPv4Address] = None
"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):
"""
@@ -25,30 +47,84 @@ class WebBrowser(Application):
This method ensures the Application is ready for a new episode, including resetting any
stateful properties or statistics, and clearing any message queues.
"""
self.domain_name = ""
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.
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 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.
"""
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.
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 receive.
:param payload: The payload to be sent.
:param session_id: The Session ID the payload is to originate from. Optional.
: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_uuid[session.uuid] = session
outbound_nic.send_frame(frame)
return outbound_nic.send_frame(frame)
def receive_frame(self, frame: Frame):
"""

View File

@@ -110,7 +110,7 @@ class SoftwareManager:
dest_ip_address: Optional[IPv4Address] = None,
dest_port: Optional[Port] = None,
session_id: Optional[str] = None,
):
) -> bool:
"""
Send a payload to the SessionManager.
@@ -119,7 +119,7 @@ class SoftwareManager:
:param dest_port: The port of the payload destination.
: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
)

View File

@@ -73,10 +73,10 @@ class DatabaseService(Service):
if self.password == password:
status_code = 200 # ok
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:
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:
status_code = 404 # service not found
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
if target_domain in self.dns_cache:
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
else:
# return False if already reattempted
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
else:
# send a request to check if domain name exists in the DNS Server
@@ -104,14 +105,13 @@ class DNSClient(Service):
self,
payload: DNSPacket,
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 be sent.
:param dest_ip_address: The ip address 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.
"""
# create DNS request packet
software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager(payload=payload, session_id=session_id)
return True
self.sys_log.info(f"{self.name}: Sending DNS request to resolve {payload.dns_request.domain_name_request}")
return super().send(
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port, session_id=session_id, **kwargs
)
def receive(
self,
@@ -133,9 +134,6 @@ class DNSClient(Service):
"""
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 session_id: The Session ID the payload is to originate from. Optional.
:return: True if successful, False otherwise.
@@ -144,12 +142,16 @@ class DNSClient(Service):
if not isinstance(payload, DNSPacket):
_LOGGER.debug(f"{payload} is not a DNSPacket")
return False
# cast payload into a DNS packet
payload: DNSPacket = payload
if payload.dns_reply is not None:
# add the IP address to the client cache
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
return True
self.sys_log.error(f"Failed to resolve domain name {payload.dns_request.domain_name_request}")
return False

View File

@@ -96,13 +96,13 @@ class DNSServer(Service):
payload: DNSPacket = payload
if payload.dns_request is not None:
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}"
)
# generate a reply with the correct DNS IP address
payload = payload.generate_reply(self.dns_lookup(payload.dns_request.domain_name_request))
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}"
)
# send reply

View File

@@ -39,9 +39,12 @@ class FTPClient(FTPServiceABC):
"""
# if client service is down, return error
if self.operating_state != ServiceOperatingState.RUNNING:
self.sys_log.error("FTP Client is not running")
payload.status_code = FTPStatusCode.ERROR
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
return super()._process_ftp_command(payload=payload, session_id=session_id, **kwargs)
@@ -49,6 +52,7 @@ class FTPClient(FTPServiceABC):
self,
dest_ip_address: Optional[IPv4Address] = None,
dest_port: Optional[Port] = Port.FTP,
session_id: Optional[str] = None,
is_reattempt: Optional[bool] = False,
) -> bool:
"""
@@ -72,20 +76,27 @@ class FTPClient(FTPServiceABC):
ftp_command=FTPCommand.PORT,
ftp_command_args=Port.FTP,
)
software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager(
payload=payload, dest_ip_address=dest_ip_address, dest_port=dest_port
)
if payload.status_code == FTPStatusCode.OK:
return True
else:
if is_reattempt:
# reattempt failed
return False
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:
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
else:
# try again
self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port, is_reattempt=True)
if is_reattempt:
# 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
)
def _disconnect_from_server(
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_file_name: str,
dest_port: Optional[Port] = Port.FTP,
session_id: Optional[str] = None,
) -> bool:
"""
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.
: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
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
# check if FTP is currently connected to IP
self.connected = self._connect_to_server(
dest_ip_address=dest_ip_address,
dest_port=dest_port,
)
self.connected = self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port)
if not self.connected:
return False
@@ -166,6 +178,7 @@ class FTPClient(FTPServiceABC):
dest_file_name=dest_file_name,
dest_ip_address=dest_ip_address,
dest_port=dest_port,
session_id=session_id,
)
# send disconnect
@@ -204,10 +217,7 @@ class FTPClient(FTPServiceABC):
:type: dest_port: Optional[Port]
"""
# check if FTP is currently connected to IP
self.connected = self._connect_to_server(
dest_ip_address=dest_ip_address,
dest_port=dest_port,
)
self.connected = self._connect_to_server(dest_ip_address=dest_ip_address, dest_port=dest_port)
if not self.connected:
return False
@@ -232,12 +242,36 @@ class FTPClient(FTPServiceABC):
# the payload should have ok status code
if payload.status_code == FTPStatusCode.OK:
self.sys_log.info(f"File {src_folder_name}/{src_file_name} found in FTP server.")
self.sys_log.info(f"{self.name}: File {src_folder_name}/{src_file_name} found in FTP server.")
return True
else:
self.sys_log.error(f"File {src_folder_name}/{src_file_name} does not exist in FTP server")
self.sys_log.error(f"{self.name}: File {src_folder_name}/{src_file_name} does not exist in FTP server")
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:
"""
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.transmission.network_layer import IPProtocol
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.core.session_manager import Session
from primaite.simulator.system.services.ftp.ftp_service import FTPServiceABC
from primaite.simulator.system.services.service import ServiceOperatingState
@@ -30,14 +29,6 @@ class FTPServer(FTPServiceABC):
super().__init__(**kwargs)
self.start()
def _get_session_details(self, session_id: str) -> Session:
"""
Returns the Session object from the given session id.
:param: session_id: ID of the session that needs details retrieved
"""
return self.software_manager.session_manager.sessions_by_uuid[session_id]
def _process_ftp_command(self, payload: FTPPacket, session_id: Optional[str] = None, **kwargs) -> FTPPacket:
"""
Process the command in the FTP Packet.
@@ -50,8 +41,11 @@ class FTPServer(FTPServiceABC):
# if server service is down, return error
if self.operating_state != ServiceOperatingState.RUNNING:
payload.status_code = FTPStatusCode.ERROR
self.sys_log.error("FTP Server not running")
return payload
self.sys_log.info(f"{self.name}: Received FTP {payload.ftp_command.name} {payload.ftp_command_args}")
if 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.network.protocols.ftp import FTPCommand, FTPPacket, FTPStatusCode
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.core.software_manager import SoftwareManager
from primaite.simulator.system.services.service import Service
@@ -54,7 +53,7 @@ class FTPServiceABC(Service, ABC):
size=file_size,
)
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']}"
)
# file should exist
@@ -103,12 +102,12 @@ class FTPServiceABC(Service, ABC):
},
packet_payload_size=file.sim_size,
)
software_manager: SoftwareManager = self.software_manager
software_manager.send_payload_to_session_manager(
self.sys_log.info(f"{self.name}: Sending file {file.folder.name}/{file.name}")
response = 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 response and payload.status_code == FTPStatusCode.OK:
return True
return False
@@ -146,3 +145,27 @@ class FTPServiceABC(Service, ABC):
except Exception as e:
self.sys_log.error(f"Unable to retrieve file from {self.sys_log.hostname}: {e}")
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.payload = payload
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):
"""Run the DataManipulationBot."""
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()
if not self.connected:
self.connect()
@@ -46,4 +48,4 @@ class DataManipulationBot(DatabaseClient):
self.query(self.payload)
self.sys_log.info(f"{self.name} payload delivered: {self.payload}")
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 ipaddress import IPv4Address
from typing import Any, Dict, Optional
from primaite import getLogger
from primaite.simulator.core import Action, ActionManager
from primaite.simulator.network.transmission.transport_layer import Port
from primaite.simulator.system.software import IOSoftware
_LOGGER = getLogger(__name__)
@@ -76,20 +78,23 @@ class Service(IOSoftware):
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.
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 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.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(
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 enum import Enum
from ipaddress import IPv4Address
from typing import Any, Dict, Optional
from primaite.simulator.core import Action, ActionManager, SimComponent
from primaite.simulator.file_system.file_system import FileSystem, Folder
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
@@ -96,6 +98,14 @@ class Software(SimComponent):
am.add_action("scan", Action(func=lambda request, context: self.scan()))
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
def describe_state(self) -> Dict:
"""
@@ -209,18 +219,27 @@ class IOSoftware(Software):
)
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.
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 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.
: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: True if successful, 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:
"""

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