use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ServerFileConfig {
pub server: ServerSettings,
pub auth: AuthConfig,
pub shell: ShellConfig,
pub sftp: SftpConfig,
pub scp: ScpConfig,
pub filter: FilterConfig,
pub audit: AuditConfig,
pub security: SecurityConfig,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ServerSettings {
#[serde(default = "default_bind_address")]
pub bind_address: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub host_keys: Vec<PathBuf>,
#[serde(default = "default_max_connections")]
pub max_connections: usize,
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(default = "default_keepalive")]
pub keepalive_interval: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct AuthConfig {
#[serde(default = "default_auth_methods")]
pub methods: Vec<AuthMethod>,
#[serde(default)]
pub publickey: PublicKeyAuthSettings,
#[serde(default)]
pub password: PasswordAuthSettings,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AuthMethod {
PublicKey,
Password,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct PublicKeyAuthSettings {
pub authorized_keys_dir: Option<PathBuf>,
pub authorized_keys_pattern: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct PasswordAuthSettings {
pub users_file: Option<PathBuf>,
#[serde(default)]
pub users: Vec<UserDefinition>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UserDefinition {
pub name: String,
pub password_hash: String,
#[serde(default)]
pub shell: Option<PathBuf>,
#[serde(default)]
pub home: Option<PathBuf>,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ShellConfig {
#[serde(default = "default_shell")]
pub default: PathBuf,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default = "default_command_timeout")]
pub command_timeout: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct SftpConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub root: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct ScpConfig {
#[serde(default = "default_true")]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct FilterConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub default_action: Option<FilterAction>,
#[serde(default)]
pub rules: Vec<FilterRule>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct FilterRule {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default)]
pub path_prefix: Option<String>,
#[serde(default)]
pub extensions: Option<Vec<String>>,
#[serde(default)]
pub directory: Option<String>,
#[serde(default)]
pub min_size: Option<u64>,
#[serde(default)]
pub max_size: Option<u64>,
#[serde(default)]
pub composite: Option<CompositeRuleConfig>,
pub action: FilterAction,
#[serde(default)]
pub operations: Option<Vec<String>>,
#[serde(default)]
pub users: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CompositeRuleConfig {
#[serde(rename = "type")]
pub logic_type: CompositeLogicType,
#[serde(default)]
pub matchers: Vec<MatcherConfig>,
#[serde(default)]
pub matcher: Option<Box<MatcherConfig>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CompositeLogicType {
And,
Or,
Not,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct MatcherConfig {
#[serde(default)]
pub pattern: Option<String>,
#[serde(default)]
pub path_prefix: Option<String>,
#[serde(default)]
pub extensions: Option<Vec<String>>,
#[serde(default)]
pub directory: Option<String>,
#[serde(default)]
pub not: Option<Box<MatcherConfig>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum FilterAction {
#[default]
Allow,
Deny,
Log,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct AuditConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub exporters: Vec<AuditExporterConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum AuditExporterConfig {
#[serde(rename = "file")]
File {
path: PathBuf,
},
#[serde(rename = "otel")]
Otel {
endpoint: String,
},
#[serde(rename = "logstash")]
Logstash {
host: String,
port: u16,
},
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default)]
pub struct SecurityConfig {
#[serde(default = "default_max_auth_attempts")]
pub max_auth_attempts: u32,
#[serde(default = "default_auth_window")]
pub auth_window: u64,
#[serde(default = "default_ban_time")]
pub ban_time: u64,
#[serde(default)]
pub whitelist_ips: Vec<String>,
#[serde(default = "default_max_sessions_per_user")]
pub max_sessions_per_user: usize,
#[serde(default = "default_idle_timeout")]
pub idle_timeout: u64,
#[serde(default)]
pub session_timeout: u64,
#[serde(default)]
pub allowed_ips: Vec<String>,
#[serde(default)]
pub blocked_ips: Vec<String>,
}
fn default_bind_address() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
2222
}
fn default_max_connections() -> usize {
100
}
fn default_timeout() -> u64 {
300
}
fn default_keepalive() -> u64 {
60
}
fn default_auth_methods() -> Vec<AuthMethod> {
vec![AuthMethod::PublicKey]
}
fn default_shell() -> PathBuf {
PathBuf::from("/bin/sh")
}
fn default_command_timeout() -> u64 {
3600
}
fn default_true() -> bool {
true
}
fn default_max_auth_attempts() -> u32 {
5
}
fn default_auth_window() -> u64 {
300
}
fn default_ban_time() -> u64 {
300
}
fn default_max_sessions_per_user() -> usize {
10
}
fn default_idle_timeout() -> u64 {
3600
}
impl Default for ServerSettings {
fn default() -> Self {
Self {
bind_address: default_bind_address(),
port: default_port(),
host_keys: Vec::new(),
max_connections: default_max_connections(),
timeout: default_timeout(),
keepalive_interval: default_keepalive(),
}
}
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
methods: default_auth_methods(),
publickey: PublicKeyAuthSettings::default(),
password: PasswordAuthSettings::default(),
}
}
}
impl Default for ShellConfig {
fn default() -> Self {
Self {
default: default_shell(),
env: HashMap::new(),
command_timeout: default_command_timeout(),
}
}
}
impl Default for SftpConfig {
fn default() -> Self {
Self {
enabled: default_true(),
root: None,
}
}
}
impl Default for ScpConfig {
fn default() -> Self {
Self {
enabled: default_true(),
}
}
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
max_auth_attempts: default_max_auth_attempts(),
auth_window: default_auth_window(),
ban_time: default_ban_time(),
whitelist_ips: Vec::new(),
max_sessions_per_user: default_max_sessions_per_user(),
idle_timeout: default_idle_timeout(),
session_timeout: 0,
allowed_ips: Vec::new(),
blocked_ips: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ServerFileConfig::default();
assert_eq!(config.server.bind_address, "0.0.0.0");
assert_eq!(config.server.port, 2222);
assert_eq!(config.server.max_connections, 100);
assert!(config.sftp.enabled);
assert!(config.scp.enabled);
assert!(!config.filter.enabled);
assert!(!config.audit.enabled);
}
#[test]
fn test_auth_method_serialization() {
let yaml = "publickey";
let method: AuthMethod = serde_yaml::from_str(yaml).unwrap();
assert_eq!(method, AuthMethod::PublicKey);
let yaml = "password";
let method: AuthMethod = serde_yaml::from_str(yaml).unwrap();
assert_eq!(method, AuthMethod::Password);
}
#[test]
fn test_filter_action_serialization() {
let yaml = "allow";
let action: FilterAction = serde_yaml::from_str(yaml).unwrap();
matches!(action, FilterAction::Allow);
let yaml = "deny";
let action: FilterAction = serde_yaml::from_str(yaml).unwrap();
matches!(action, FilterAction::Deny);
let yaml = "log";
let action: FilterAction = serde_yaml::from_str(yaml).unwrap();
matches!(action, FilterAction::Log);
}
#[test]
fn test_yaml_parsing_minimal() {
let yaml = r#"
server:
port: 2222
host_keys:
- /etc/bssh/host_key
"#;
let config: ServerFileConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.server.port, 2222);
assert_eq!(config.server.host_keys.len(), 1);
}
#[test]
fn test_yaml_parsing_comprehensive() {
let yaml = r#"
server:
bind_address: "127.0.0.1"
port: 2223
host_keys:
- /etc/bssh/ssh_host_ed25519_key
- /etc/bssh/ssh_host_rsa_key
max_connections: 50
timeout: 600
keepalive_interval: 30
auth:
methods:
- publickey
- password
publickey:
authorized_keys_pattern: "/home/{user}/.ssh/authorized_keys"
password:
users:
- name: testuser
password_hash: "$6$rounds=656000$..."
shell: /bin/bash
shell:
default: /bin/bash
command_timeout: 7200
env:
LANG: en_US.UTF-8
security:
max_auth_attempts: 3
ban_time: 600
allowed_ips:
- "192.168.1.0/24"
"#;
let config: ServerFileConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.server.bind_address, "127.0.0.1");
assert_eq!(config.server.port, 2223);
assert_eq!(config.server.host_keys.len(), 2);
assert_eq!(config.auth.methods.len(), 2);
assert_eq!(config.shell.default, PathBuf::from("/bin/bash"));
assert_eq!(config.security.max_auth_attempts, 3);
assert_eq!(config.security.allowed_ips.len(), 1);
}
}