biscuit_ssh_core/
request.rs1use std::fmt;
8use std::net::IpAddr;
9use std::str::FromStr;
10use std::time::SystemTime;
11
12use crate::Error;
13
14const MAX_NAME_LEN: usize = 64;
16
17const MAX_COMMAND_LEN: usize = 1024;
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct Name(String);
24
25impl Name {
26 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30}
31
32impl FromStr for Name {
33 type Err = Error;
34
35 fn from_str(s: &str) -> Result<Self, Error> {
36 if s.is_empty() || s.len() > MAX_NAME_LEN {
37 return Err(Error::invalid("name", "must be 1 to 64 characters long"));
38 }
39 if s.starts_with('-') {
40 return Err(Error::invalid("name", "must not start with '-'"));
41 }
42 if !s
43 .bytes()
44 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
45 {
46 return Err(Error::invalid("name", "allowed characters are A-Z a-z 0-9 . _ -"));
47 }
48 Ok(Self(s.to_owned()))
49 }
50}
51
52impl fmt::Display for Name {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct Command(String);
62
63impl Command {
64 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl FromStr for Command {
71 type Err = Error;
72
73 fn from_str(s: &str) -> Result<Self, Error> {
74 if s.trim().is_empty() || s.len() > MAX_COMMAND_LEN {
75 return Err(Error::invalid("command", "must be 1 to 1024 bytes long"));
76 }
77 if s.chars().any(char::is_control) {
78 return Err(Error::invalid("command", "must not contain control characters"));
79 }
80 Ok(Self(s.to_owned()))
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum Mode {
87 Certificate,
89 Bastion,
91}
92
93impl Mode {
94 pub fn as_str(self) -> &'static str {
96 match self {
97 Self::Certificate => "certificate",
98 Self::Bastion => "bastion",
99 }
100 }
101}
102
103#[derive(Debug, Clone)]
108pub struct AccessRequest {
109 pub time: SystemTime,
111 pub source_ip: IpAddr,
113 pub server: Name,
115 pub user: Name,
117 pub command: Option<Command>,
119 pub public_key: String,
123 pub mode: Mode,
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn validates_names() {
133 for ok in ["web-1", "deploy", "db.eu_west.2", "a"] {
134 assert!(ok.parse::<Name>().is_ok(), "{ok}");
135 }
136 let long = "a".repeat(65);
137 for bad in ["", "-rf", "web:1", "a b", "é", "a,b", "a/b", long.as_str()] {
138 assert!(bad.parse::<Name>().is_err(), "{bad}");
139 }
140 }
141
142 #[test]
143 fn validates_commands() {
144 assert!("uptime".parse::<Command>().is_ok());
145 assert!("rsync --server -e.LsfxC . /srv".parse::<Command>().is_ok());
146 let long = "a".repeat(1025);
147 for bad in ["", " ", "ls\nrm -rf /", "ls\0", long.as_str()] {
148 assert!(bad.parse::<Command>().is_err(), "{bad:?}");
149 }
150 }
151}