use std::fmt;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::SystemTime;
use crate::Error;
const MAX_NAME_LEN: usize = 64;
const MAX_COMMAND_LEN: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Name(String);
impl Name {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Name {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.is_empty() || s.len() > MAX_NAME_LEN {
return Err(Error::invalid("name", "must be 1 to 64 characters long"));
}
if s.starts_with('-') {
return Err(Error::invalid("name", "must not start with '-'"));
}
if !s
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
{
return Err(Error::invalid("name", "allowed characters are A-Z a-z 0-9 . _ -"));
}
Ok(Self(s.to_owned()))
}
}
impl fmt::Display for Name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command(String);
impl Command {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Command {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
if s.trim().is_empty() || s.len() > MAX_COMMAND_LEN {
return Err(Error::invalid("command", "must be 1 to 1024 bytes long"));
}
if s.chars().any(char::is_control) {
return Err(Error::invalid("command", "must not contain control characters"));
}
Ok(Self(s.to_owned()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Certificate,
Bastion,
}
impl Mode {
pub fn as_str(self) -> &'static str {
match self {
Self::Certificate => "certificate",
Self::Bastion => "bastion",
}
}
}
#[derive(Debug, Clone)]
pub struct AccessRequest {
pub time: SystemTime,
pub source_ip: IpAddr,
pub server: Name,
pub user: Name,
pub command: Option<Command>,
pub public_key: String,
pub mode: Mode,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validates_names() {
for ok in ["web-1", "deploy", "db.eu_west.2", "a"] {
assert!(ok.parse::<Name>().is_ok(), "{ok}");
}
let long = "a".repeat(65);
for bad in ["", "-rf", "web:1", "a b", "é", "a,b", "a/b", long.as_str()] {
assert!(bad.parse::<Name>().is_err(), "{bad}");
}
}
#[test]
fn validates_commands() {
assert!("uptime".parse::<Command>().is_ok());
assert!("rsync --server -e.LsfxC . /srv".parse::<Command>().is_ok());
let long = "a".repeat(1025);
for bad in ["", " ", "ls\nrm -rf /", "ls\0", long.as_str()] {
assert!(bad.parse::<Command>().is_err(), "{bad:?}");
}
}
}