#2711 - Initial commit of Terminal Service Skeleton framework. Added in a placeholder SSHPacket class. Currently, this allows the Terminal 'service' to be installed onto a HostNode class, and Port 22 - SSH to be visible when using .show(). Functionality and testing still to be completed
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
.. only:: comment
|
||||
|
||||
© Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
|
||||
.. _Terminal:
|
||||
|
||||
Terminal
|
||||
########
|
||||
|
||||
The ``Terminal`` provides a generic terminal simulation, by extending the base Service class
|
||||
|
||||
Key capabilities
|
||||
================
|
||||
|
||||
- Authenticates User connection by maintaining an active User account.
|
||||
- Ensures packets are matched to an existing session
|
||||
- Simulates common Terminal commands
|
||||
- Leverages the Service base class for install/uninstall, status tracking etc.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
- Install on a node via the ``SoftwareManager`` to start the Terminal
|
||||
- Terminal Clients connect, execute commands and disconnect.
|
||||
- Service runs on SSH port 22 by default.
|
||||
@@ -38,6 +38,7 @@ 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.ntp.ntp_client import NTPClient
|
||||
from primaite.simulator.system.services.ntp.ntp_server import NTPServer
|
||||
from primaite.simulator.system.services.terminal.terminal import Terminal
|
||||
from primaite.simulator.system.services.web_server.web_server import WebServer
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
@@ -60,6 +61,7 @@ SERVICE_TYPES_MAPPING = {
|
||||
"FTPServer": FTPServer,
|
||||
"NTPClient": NTPClient,
|
||||
"NTPServer": NTPServer,
|
||||
"Terminal": Terminal,
|
||||
}
|
||||
"""List of available services that can be installed on nodes in the PrimAITE Simulation."""
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from primaite.simulator.system.services.arp.arp import ARP, ARPPacket
|
||||
from primaite.simulator.system.services.dns.dns_client import DNSClient
|
||||
from primaite.simulator.system.services.icmp.icmp import ICMP
|
||||
from primaite.simulator.system.services.ntp.ntp_client import NTPClient
|
||||
from primaite.simulator.system.services.terminal.terminal import Terminal
|
||||
from primaite.utils.validators import IPV4Address
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
@@ -306,6 +307,7 @@ class HostNode(Node):
|
||||
"NTPClient": NTPClient,
|
||||
"WebBrowser": WebBrowser,
|
||||
"NMAP": NMAP,
|
||||
"Terminal": Terminal,
|
||||
}
|
||||
"""List of system software that is automatically installed on nodes."""
|
||||
|
||||
|
||||
71
src/primaite/simulator/network/protocols/ssh.py
Normal file
71
src/primaite/simulator/network/protocols/ssh.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import Dict, Optional
|
||||
|
||||
from primaite.interface.request import RequestResponse
|
||||
from primaite.simulator.network.protocols.packet import DataPacket
|
||||
|
||||
# TODO: Elaborate / Confirm / Validate - See 2709.
|
||||
# Placeholder implementation for Terminal Class implementation.
|
||||
|
||||
|
||||
class SSHTransportMessage(IntEnum):
|
||||
"""
|
||||
Enum list of Transport layer messages that can be handled by the simulation.
|
||||
|
||||
Each msg value is equivalent to the real-world.
|
||||
"""
|
||||
|
||||
SSH_MSG_USERAUTH_REQUEST = 50
|
||||
"""Requests User Authentication."""
|
||||
|
||||
SSH_MSG_USERAUTH_FAILURE = 51
|
||||
"""Indicates User Authentication failed."""
|
||||
|
||||
SSH_MSG_USERAUTH_SUCCESS = 52
|
||||
"""Indicates User Authentication failed was successful."""
|
||||
|
||||
SSH_MSG_SERVICE_REQUEST = 24
|
||||
"""Requests a service - such as executing a command."""
|
||||
|
||||
# These two msgs are invented for primAITE however are modelled on reality
|
||||
|
||||
SSH_MSG_SERVICE_FAILED = 25
|
||||
"""Indicates that the requested service failed."""
|
||||
|
||||
SSH_MSG_SERVICE_SUCCESS = 26
|
||||
"""Indicates that the requested service was successful."""
|
||||
|
||||
|
||||
class SSHConnectionMessage(IntEnum):
|
||||
"""Int Enum list of all SSH's connection protocol messages that can be handled by the simulation."""
|
||||
|
||||
SSH_MSG_CHANNEL_OPEN = 80
|
||||
"""Requests an open channel - Used in combination with SSH_MSG_USERAUTH_REQUEST."""
|
||||
|
||||
SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 81
|
||||
"""Confirms an open channel."""
|
||||
|
||||
SSH_MSG_CHANNEL_OPEN_FAILED = 82
|
||||
"""Indicates that channel opening failure."""
|
||||
|
||||
SSH_MSG_CHANNEL_DATA = 84
|
||||
"""Indicates that data is being sent through the channel."""
|
||||
|
||||
SSH_MSG_CHANNEL_CLOSE = 87
|
||||
"""Closes the channel."""
|
||||
|
||||
|
||||
class SSHPacket(DataPacket):
|
||||
"""Represents an SSHPacket."""
|
||||
|
||||
transport_message: SSHTransportMessage
|
||||
|
||||
connection_message: SSHConnectionMessage
|
||||
|
||||
ssh_command: Optional[any] = None # This is the request string
|
||||
|
||||
ssh_output: Optional[RequestResponse] = None # The Request Manager's returned RequestResponse
|
||||
|
||||
user_account: Optional[Dict] = None # The user account we will use to login if we do not have a current connection.
|
||||
@@ -0,0 +1 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
190
src/primaite/simulator/system/services/terminal/terminal.py
Normal file
190
src/primaite/simulator/system/services/terminal/terminal.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# © Crown-owned copyright 2024, Defence Science and Technology Laboratory UK
|
||||
from __future__ import annotations
|
||||
|
||||
from ipaddress import IPv4Address, IPv4Network
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from primaite.interface.request import RequestFormat, RequestResponse
|
||||
from primaite.simulator.core import RequestManager, RequestPermissionValidator
|
||||
from primaite.simulator.network.protocols.icmp import ICMPPacket
|
||||
|
||||
# from primaite.simulator.network.protocols.ssh import SSHPacket, SSHTransportMessage, SSHConnectionMessage
|
||||
from primaite.simulator.network.transmission.network_layer import IPProtocol
|
||||
from primaite.simulator.network.transmission.transport_layer import Port
|
||||
from primaite.simulator.system.services.service import Service, ServiceOperatingState
|
||||
|
||||
|
||||
class Terminal(Service):
|
||||
"""Class used to simulate a generic terminal service. Can be interacted with by other terminals via SSH."""
|
||||
|
||||
user_account: Optional[str] = None
|
||||
"The User Account used for login"
|
||||
|
||||
connected: bool = False
|
||||
"Boolean Value for whether connected"
|
||||
|
||||
connection_uuid: Optional[str] = None
|
||||
"Uuid for connection requests"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["name"] = "Terminal"
|
||||
kwargs["port"] = Port.SSH
|
||||
kwargs["protocol"] = IPProtocol.TCP
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.operating_state = ServiceOperatingState.RUNNING
|
||||
|
||||
class _LoginValidator(RequestPermissionValidator):
|
||||
"""
|
||||
When requests come in, this validator will only let them through if we have valid login credentials.
|
||||
|
||||
This should ensure that no actions are resolved without valid user credentials.
|
||||
"""
|
||||
|
||||
terminal: Terminal
|
||||
|
||||
def __call__(self, request: RequestFormat, context: Dict) -> bool:
|
||||
"""Return whether the login credentials are valid."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def fail_message(self) -> str:
|
||||
"""Message that is reported when a request is rejected by this validator."""
|
||||
return (
|
||||
f"Cannot perform request on Terminal '{self.terminal.hostname}' because login credentials are invalid"
|
||||
)
|
||||
|
||||
def _validate_login(self) -> bool:
|
||||
"""Validate login credentials when receiving commands."""
|
||||
# TODO: Implement
|
||||
return True
|
||||
|
||||
def receive_payload_from_software_manager(
|
||||
self,
|
||||
payload: Any,
|
||||
dst_ip_address: Optional[Union[IPv4Address, IPv4Network]] = None,
|
||||
src_port: Optional[Port] = None,
|
||||
dst_port: Optional[Port] = None,
|
||||
session_id: Optional[str] = None,
|
||||
ip_protocol: IPProtocol = IPProtocol.TCP,
|
||||
icmp_packet: Optional[ICMPPacket] = None,
|
||||
connection_id: Optional[str] = None,
|
||||
) -> Union[Any, None]:
|
||||
"""Receive Software Manager Payload."""
|
||||
self._validate_login()
|
||||
|
||||
def _init_request_manager(self) -> RequestManager:
|
||||
"""Initialise Request manager."""
|
||||
# _login_is_valid = Terminal._LoginValidator(terminal=self)
|
||||
rm = super()._init_request_manager()
|
||||
|
||||
return rm
|
||||
|
||||
def send(
|
||||
self,
|
||||
payload: Any,
|
||||
dest_ip_address: Optional[IPv4Address] = None,
|
||||
session_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""Send Request to Software Manager."""
|
||||
return super().send(payload=payload, dest_ip_address=dest_ip_address, dest_port=Port.SSH, session_id=session_id)
|
||||
|
||||
def describe_state(self) -> Dict:
|
||||
"""
|
||||
Produce a dictionary describing the current state of this object.
|
||||
|
||||
Please see :py:meth:`primaite.simulator.core.SimComponent.describe_state` for a more detailed explanation.
|
||||
|
||||
:return: Current state of this object and child objects.
|
||||
:rtype: Dict
|
||||
"""
|
||||
state = super().describe_state()
|
||||
# TBD
|
||||
state.update({"hostname": self.hostname})
|
||||
return state
|
||||
|
||||
def execute(self, command: Any, request: Any) -> Optional[RequestResponse]:
|
||||
"""Execute Command."""
|
||||
# Returning the request to the request manager.
|
||||
if self._validate_login():
|
||||
return self.apply_request(request)
|
||||
else:
|
||||
self.sys_log.error("Invalid login credentials provided.")
|
||||
return None
|
||||
|
||||
def apply_request(self, request: List[str | int | float | Dict], context: Dict | None = None) -> RequestResponse:
|
||||
"""Apply Temrinal Request."""
|
||||
return super().apply_request(request, context)
|
||||
|
||||
def login(self, dest_ip_address: IPv4Address) -> bool:
|
||||
"""
|
||||
Perform an initial login request.
|
||||
|
||||
If this fails, raises an error.
|
||||
"""
|
||||
# TODO: This will need elaborating when user accounts are implemented
|
||||
self.sys_log.info("Attempting Login")
|
||||
self._ssh_process_login(self, dest_ip_address=dest_ip_address, user_account=self.user_account)
|
||||
|
||||
def _generate_connection_id(self) -> str:
|
||||
"""Generate a unique connection ID."""
|
||||
return str(uuid4())
|
||||
|
||||
# %%
|
||||
|
||||
# def _ssh_process_login(self, user_account: dict, **kwargs) -> SSHPacket:
|
||||
# """Processes the login attempt. Returns a SSHPacket which either rejects the login or accepts it."""
|
||||
# # we assume that the login fails unless we meet all the criteria.
|
||||
# transport_message = SSHTransportMessage.SSH_MSG_USERAUTH_FAILURE
|
||||
# connection_message = SSHConnectionMessage.SSH_MSG_CHANNEL_OPEN_FAILED
|
||||
# # operating state validation here(if overwhelmed)
|
||||
|
||||
# # Hard coded at current - replace with another method to handle local accounts.
|
||||
# if user_account == f"{self.user_name:} placeholder, {self.password:} placeholder": # hardcoded
|
||||
# connection_id = self._generate_connection_id()
|
||||
# if not self.add_connection(self, connection_id="ssh_connection", session_id=self.session_id):
|
||||
# self.sys_log.warning(f"{self.name}: Connect request for {self.src_ip} declined.
|
||||
# Service is at capacity.")
|
||||
# ...
|
||||
# else:
|
||||
# self.sys_log.info(f"{self.name}: Connect request for {connection_id=} authorised")
|
||||
# transport_message = SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS
|
||||
# connection_message = SSHConnectionMessage.SSH_MSG_CHANNEL_OPEN_CONFIRMATION
|
||||
|
||||
# payload: SSHPacket = SSHPacket(transport_message = transport_message, connection_message = connection_message)
|
||||
# return payload
|
||||
|
||||
# %%
|
||||
# Copy + Paste from Terminal Wiki
|
||||
|
||||
# def ssh_remote_login(self, dest_ip_address = IPv4Address, user_account: Optional[dict] = None) -> bool:
|
||||
# if user_account:
|
||||
# # Setting default creds (Best to use this until we have more clarification on the specifics of user accounts)
|
||||
# self.user_account = {self.user_name:"placeholder", self.password:"placeholder"}
|
||||
|
||||
# # Implement SSHPacket class
|
||||
# payload: SSHPacket = SSHPacket(transport_message= SSHTransportMessage.SSH_MSG_USERAUTH_REQUEST,
|
||||
# connection_message= SSHConnectionMessage.SSH_MSG_CHANNEL_OPEN,
|
||||
# user_account=user_account)
|
||||
# if self.send(payload=payload,dest_ip_address=dest_ip_address):
|
||||
# if payload.connection_message == SSHTransportMessage.SSH_MSG_USERAUTH_SUCCESS:
|
||||
# self.sys_log.info(f"{self.name} established an ssh connection with {dest_ip_address}")
|
||||
# # Need to confirm if self.uuid is correct.
|
||||
# self.add_connection(self, connection_id=self.uuid, session_id=self.session_id)
|
||||
# return True
|
||||
# else:
|
||||
# self.sys_log.error("Payload type incorrect, Login Failed")
|
||||
# return False
|
||||
# else:
|
||||
# self.sys_log.error("Incorrect credentials provided. Login Failed.")
|
||||
# return False
|
||||
# %%
|
||||
|
||||
def connect(self, **kwargs):
|
||||
"""Send connect request."""
|
||||
self._connect(self, **kwargs)
|
||||
|
||||
def _connect(self):
|
||||
"""Do something."""
|
||||
pass
|
||||
Reference in New Issue
Block a user