use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::parser::{tokenize, ParsedLine};
use super::ConfigError;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SshServerConfig {
pub port: Option<u16>,
pub listen_addresses: Vec<String>,
pub host_key_files: Vec<String>,
pub authorized_keys_file: Option<String>,
pub allow_users: Vec<String>,
pub login_grace_time: Option<u32>,
pub max_startups: Option<u32>,
pub allow_agent_forwarding: Option<bool>,
pub x11_forwarding: Option<bool>,
pub accept_env: Vec<String>,
pub strict_modes: Option<bool>,
pub log_level: Option<u8>,
pub sftp_enabled: Option<bool>,
pub sftp_read_only: Option<bool>,
pub sftp_root: Option<String>,
pub scp_enabled: Option<bool>,
}
impl SshServerConfig {
pub fn parse(src: &str) -> Result<Self, ConfigError> {
let lines = tokenize(src)?;
let mut out = SshServerConfig::default();
for line in lines {
if line.keyword == "match" {
return Err(ConfigError::Unsupported {
line: line.line_no,
msg: "sshd_config Match blocks not yet supported".into(),
});
}
apply_keyword(&mut out, &line)?;
}
Ok(out)
}
}
fn apply_keyword(opts: &mut SshServerConfig, line: &ParsedLine) -> Result<(), ConfigError> {
let kw = line.keyword.as_str();
match kw {
"port" => {
opts.port = Some(parse_u16(line)?);
}
"listenaddress" => {
opts.listen_addresses.push(one_arg(line)?);
}
"hostkey" => {
opts.host_key_files.push(one_arg(line)?);
}
"authorizedkeysfile" => {
opts.authorized_keys_file = Some(one_arg(line)?);
}
"allowusers" => {
for u in &line.args {
opts.allow_users.push(u.clone());
}
if line.args.is_empty() {
return Err(ConfigError::BadValue {
line: line.line_no,
keyword: kw.to_string(),
msg: "expected at least one user name".into(),
});
}
}
"logingracetime" => {
opts.login_grace_time = Some(parse_duration_seconds(line)?);
}
"maxstartups" => {
let s = one_arg(line)?;
if s.contains(':') {
return Err(ConfigError::Unsupported {
line: line.line_no,
msg: "MaxStartups start:rate:full triple not yet supported".into(),
});
}
opts.max_startups = Some(s.parse::<u32>().map_err(|_| ConfigError::BadValue {
line: line.line_no,
keyword: kw.to_string(),
msg: format!("expected an integer, got {s:?}"),
})?);
}
"allowagentforwarding" => {
opts.allow_agent_forwarding = Some(parse_yes_no(line)?);
}
"x11forwarding" => {
opts.x11_forwarding = Some(parse_yes_no(line)?);
}
"acceptenv" => {
if line.args.is_empty() {
return Err(ConfigError::BadValue {
line: line.line_no,
keyword: kw.to_string(),
msg: "expected at least one env pattern".into(),
});
}
for p in &line.args {
opts.accept_env.push(p.clone());
}
}
"strictmodes" => {
opts.strict_modes = Some(parse_yes_no(line)?);
}
"loglevel" => {
opts.log_level = Some(parse_log_level(line)?);
}
"sftpenabled" => {
opts.sftp_enabled = Some(parse_yes_no(line)?);
}
"sftpreadonly" => {
opts.sftp_read_only = Some(parse_yes_no(line)?);
}
"sftproot" => {
opts.sftp_root = Some(one_arg(line)?);
}
"scpenabled" => {
opts.scp_enabled = Some(parse_yes_no(line)?);
}
_ => {
return Err(ConfigError::UnknownKeyword {
line: line.line_no,
keyword: kw.to_string(),
});
}
}
Ok(())
}
fn one_arg(line: &ParsedLine) -> Result<String, ConfigError> {
if line.args.len() != 1 {
return Err(ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("expected 1 value, got {}", line.args.len()),
});
}
Ok(line.args[0].clone())
}
fn parse_u16(line: &ParsedLine) -> Result<u16, ConfigError> {
let s = one_arg(line)?;
s.parse::<u16>().map_err(|_| ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("expected a port number, got {s:?}"),
})
}
fn parse_yes_no(line: &ParsedLine) -> Result<bool, ConfigError> {
let s = one_arg(line)?.to_ascii_lowercase();
match s.as_str() {
"yes" | "true" | "on" => Ok(true),
"no" | "false" | "off" => Ok(false),
_ => Err(ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("expected yes/no, got {s:?}"),
}),
}
}
fn parse_log_level(line: &ParsedLine) -> Result<u8, ConfigError> {
let s = one_arg(line)?.to_ascii_uppercase();
match s.as_str() {
"QUIET" | "FATAL" | "ERROR" | "INFO" => Ok(0),
"VERBOSE" | "DEBUG" | "DEBUG1" => Ok(1),
"DEBUG2" => Ok(2),
"DEBUG3" => Ok(3),
_ => Err(ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("expected QUIET..DEBUG3, got {s:?}"),
}),
}
}
fn parse_duration_seconds(line: &ParsedLine) -> Result<u32, ConfigError> {
let s = one_arg(line)?;
let bytes = s.as_bytes();
let mut total: u64 = 0;
let mut acc: u64 = 0;
let mut has_digit = false;
for &b in bytes {
if b.is_ascii_digit() {
has_digit = true;
acc = acc * 10 + (b - b'0') as u64;
} else {
let mult: u64 = match b.to_ascii_lowercase() {
b's' => 1,
b'm' => 60,
b'h' => 3600,
b'd' => 86400,
b'w' => 604800,
_ => {
return Err(ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("bad duration unit in {s:?}"),
});
}
};
if !has_digit {
return Err(ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("missing number before unit in {s:?}"),
});
}
total = total.saturating_add(acc.saturating_mul(mult));
acc = 0;
has_digit = false;
}
}
if has_digit {
total = total.saturating_add(acc);
}
if total > u32::MAX as u64 {
return Err(ConfigError::BadValue {
line: line.line_no,
keyword: line.keyword.clone(),
msg: format!("duration overflows u32 seconds: {s:?}"),
});
}
Ok(total as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minimal() {
let src = "\
Port 2222
ListenAddress 127.0.0.1
HostKey /etc/ssh/ssh_host_ed25519_key
AuthorizedKeysFile /etc/authkeys
AllowUsers alice bob
StrictModes yes
";
let cfg = SshServerConfig::parse(src).unwrap();
assert_eq!(cfg.port, Some(2222));
assert_eq!(cfg.listen_addresses, vec!["127.0.0.1".to_string()]);
assert_eq!(
cfg.host_key_files,
vec!["/etc/ssh/ssh_host_ed25519_key".to_string()]
);
assert_eq!(cfg.authorized_keys_file.as_deref(), Some("/etc/authkeys"));
assert_eq!(
cfg.allow_users,
vec!["alice".to_string(), "bob".to_string()]
);
assert_eq!(cfg.strict_modes, Some(true));
}
#[test]
fn cumulative_fields() {
let src = "\
HostKey /a
HostKey /b
ListenAddress 127.0.0.1
ListenAddress ::1
AllowUsers alice
AllowUsers bob carol
";
let cfg = SshServerConfig::parse(src).unwrap();
assert_eq!(cfg.host_key_files, vec!["/a".to_string(), "/b".to_string()]);
assert_eq!(
cfg.listen_addresses,
vec!["127.0.0.1".to_string(), "::1".to_string()]
);
assert_eq!(
cfg.allow_users,
vec!["alice".to_string(), "bob".to_string(), "carol".to_string()]
);
}
#[test]
fn match_block_unsupported() {
let src = "Port 22\nMatch User alice\n AllowAgentForwarding no\n";
let err = SshServerConfig::parse(src).unwrap_err();
match err {
ConfigError::Unsupported { line, .. } => assert_eq!(line, 2),
_ => panic!("wrong error: {err:?}"),
}
}
#[test]
fn unknown_keyword_errors() {
let src = "Port 22\nKexAlgorithms curve25519-sha256\n";
let err = SshServerConfig::parse(src).unwrap_err();
match err {
ConfigError::UnknownKeyword { keyword, line } => {
assert_eq!(keyword, "kexalgorithms");
assert_eq!(line, 2);
}
_ => panic!("wrong error: {err:?}"),
}
}
#[test]
fn login_grace_time_units() {
for (s, want) in [
("30", 30u32),
("30s", 30),
("2m", 120),
("1h", 3600),
("1m30s", 90),
] {
let src = format!("LoginGraceTime {s}\n");
let cfg = SshServerConfig::parse(&src).unwrap();
assert_eq!(cfg.login_grace_time, Some(want), "case {s:?}");
}
}
#[test]
fn sftp_knobs() {
let src = "\
SftpEnabled yes
SftpReadOnly no
SftpRoot /var/sftp
ScpEnabled yes
";
let cfg = SshServerConfig::parse(src).unwrap();
assert_eq!(cfg.sftp_enabled, Some(true));
assert_eq!(cfg.sftp_read_only, Some(false));
assert_eq!(cfg.sftp_root.as_deref(), Some("/var/sftp"));
assert_eq!(cfg.scp_enabled, Some(true));
}
}