use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SessionType {
#[default]
StandardStream,
Port,
}
pub trait SsmDocument {
fn document_name(&self) -> &'static str;
fn session_type(&self) -> SessionType;
fn parameters(&self) -> HashMap<String, Vec<String>>;
}
fn params<const N: usize>(pairs: [(&str, String); N]) -> HashMap<String, Vec<String>> {
pairs
.into_iter()
.map(|(k, v)| (k.to_owned(), vec![v]))
.collect()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ShellSession;
impl ShellSession {
pub const fn new() -> Self {
Self
}
}
impl SsmDocument for ShellSession {
fn document_name(&self) -> &'static str {
""
}
fn session_type(&self) -> SessionType {
SessionType::StandardStream
}
fn parameters(&self) -> HashMap<String, Vec<String>> {
HashMap::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PortForwardingSession {
pub remote_port: u16,
}
impl PortForwardingSession {
pub const DOCUMENT_NAME: &'static str = "AWS-StartPortForwardingSession";
pub const fn new(remote_port: u16) -> Self {
Self { remote_port }
}
}
impl SsmDocument for PortForwardingSession {
fn document_name(&self) -> &'static str {
Self::DOCUMENT_NAME
}
fn session_type(&self) -> SessionType {
SessionType::Port
}
fn parameters(&self) -> HashMap<String, Vec<String>> {
params([("portNumber", self.remote_port.to_string())])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortForwardingToRemoteHost {
pub host: String,
pub remote_port: u16,
}
impl PortForwardingToRemoteHost {
pub const DOCUMENT_NAME: &'static str = "AWS-StartPortForwardingSessionToRemoteHost";
pub fn new(host: impl Into<String>, remote_port: u16) -> Self {
Self {
host: host.into(),
remote_port,
}
}
}
impl SsmDocument for PortForwardingToRemoteHost {
fn document_name(&self) -> &'static str {
Self::DOCUMENT_NAME
}
fn session_type(&self) -> SessionType {
SessionType::Port
}
fn parameters(&self) -> HashMap<String, Vec<String>> {
params([
("host", self.host.clone()),
("portNumber", self.remote_port.to_string()),
])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SshSession {
pub port: u16,
}
impl SshSession {
pub const DOCUMENT_NAME: &'static str = "AWS-StartSSHSession";
pub const fn new() -> Self {
Self { port: 22 }
}
pub const fn on_port(port: u16) -> Self {
Self { port }
}
}
impl Default for SshSession {
fn default() -> Self {
Self::new()
}
}
impl SsmDocument for SshSession {
fn document_name(&self) -> &'static str {
Self::DOCUMENT_NAME
}
fn session_type(&self) -> SessionType {
SessionType::StandardStream
}
fn parameters(&self) -> HashMap<String, Vec<String>> {
params([("portNumber", self.port.to_string())])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InteractiveCommand {
pub command: String,
}
impl InteractiveCommand {
pub const DOCUMENT_NAME: &'static str = "AWS-StartInteractiveCommand";
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
}
}
}
impl SsmDocument for InteractiveCommand {
fn document_name(&self) -> &'static str {
Self::DOCUMENT_NAME
}
fn session_type(&self) -> SessionType {
SessionType::StandardStream
}
fn parameters(&self) -> HashMap<String, Vec<String>> {
params([("command", self.command.clone())])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonInteractiveCommand {
pub command: String,
}
impl NonInteractiveCommand {
pub const DOCUMENT_NAME: &'static str = "AWS-StartNonInteractiveCommand";
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
}
}
}
impl SsmDocument for NonInteractiveCommand {
fn document_name(&self) -> &'static str {
Self::DOCUMENT_NAME
}
fn session_type(&self) -> SessionType {
SessionType::StandardStream
}
fn parameters(&self) -> HashMap<String, Vec<String>> {
params([("command", self.command.clone())])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_sessions_carry_no_document() {
let doc = ShellSession::new();
assert!(doc.document_name().is_empty());
assert!(doc.parameters().is_empty());
assert_eq!(doc.session_type(), SessionType::StandardStream);
}
#[test]
fn port_forwarding_uses_the_documented_parameter_names() {
let doc = PortForwardingSession::new(3389);
assert_eq!(doc.document_name(), "AWS-StartPortForwardingSession");
assert_eq!(doc.parameters()["portNumber"], vec!["3389".to_string()]);
assert_eq!(doc.session_type(), SessionType::Port);
}
#[test]
fn remote_host_forwarding_sends_host_and_port() {
let doc = PortForwardingToRemoteHost::new("db.internal", 5432);
let p = doc.parameters();
assert_eq!(
doc.document_name(),
"AWS-StartPortForwardingSessionToRemoteHost"
);
assert_eq!(p["host"], vec!["db.internal".to_string()]);
assert_eq!(p["portNumber"], vec!["5432".to_string()]);
assert_eq!(doc.session_type(), SessionType::Port);
}
#[test]
fn ssh_is_a_byte_stream_not_a_multiplexed_forward() {
assert_eq!(
SshSession::new().session_type(),
SessionType::StandardStream
);
assert_eq!(
SshSession::new().parameters()["portNumber"],
vec!["22".to_string()]
);
assert_eq!(
SshSession::on_port(2222).parameters()["portNumber"],
vec!["2222".to_string()]
);
}
#[test]
fn command_documents_pass_the_command_through() {
assert_eq!(
InteractiveCommand::new("top -b").parameters()["command"],
vec!["top -b".to_string()]
);
assert_eq!(
NonInteractiveCommand::new("uname -a").parameters()["command"],
vec!["uname -a".to_string()]
);
assert_eq!(
NonInteractiveCommand::DOCUMENT_NAME,
"AWS-StartNonInteractiveCommand"
);
}
#[test]
fn exactly_two_documents_are_port_sessions() {
let port_types = [
PortForwardingSession::new(1).session_type(),
PortForwardingToRemoteHost::new("h", 1).session_type(),
];
assert!(port_types.iter().all(|t| *t == SessionType::Port));
let stream_types = [
ShellSession::new().session_type(),
SshSession::new().session_type(),
InteractiveCommand::new("x").session_type(),
NonInteractiveCommand::new("x").session_type(),
];
assert!(stream_types
.iter()
.all(|t| *t == SessionType::StandardStream));
}
}