mod loader;
mod types;
pub use loader::{generate_config_template, load_config};
pub use types::*;
use super::auth::{
AuthProvider, CompositeAuthProvider, PasswordAuthConfig, PublicKeyAuthConfig, PublicKeyVerifier,
};
use super::exec::ExecConfig;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default)]
pub host_keys: Vec<PathBuf>,
#[serde(default = "default_listen_address")]
pub listen_address: String,
#[serde(default = "default_max_connections")]
pub max_connections: usize,
#[serde(default = "default_max_auth_attempts")]
pub max_auth_attempts: u32,
#[serde(default = "default_auth_timeout_secs")]
pub auth_timeout_secs: u64,
#[serde(default = "default_idle_timeout_secs")]
pub idle_timeout_secs: u64,
#[serde(default)]
pub allow_password_auth: bool,
#[serde(default = "default_true")]
pub allow_publickey_auth: bool,
#[serde(default)]
pub allow_keyboard_interactive: bool,
#[serde(default)]
pub banner: Option<String>,
#[serde(default)]
pub publickey_auth: PublicKeyAuthConfigSerde,
#[serde(default)]
pub password_auth: PasswordAuthConfigSerde,
#[serde(default)]
pub exec: ExecConfig,
#[serde(default = "default_true")]
pub scp_enabled: bool,
#[serde(default = "default_auth_window_secs")]
pub auth_window_secs: u64,
#[serde(default = "default_ban_time_secs")]
pub ban_time_secs: u64,
#[serde(default)]
pub whitelist_ips: Vec<String>,
#[serde(default)]
pub allowed_ips: Vec<String>,
#[serde(default)]
pub blocked_ips: Vec<String>,
#[serde(default = "default_max_sessions_per_user")]
pub max_sessions_per_user: usize,
#[serde(default)]
pub session_timeout_secs: u64,
}
fn default_max_sessions_per_user() -> usize {
10
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PublicKeyAuthConfigSerde {
pub authorized_keys_dir: Option<PathBuf>,
pub authorized_keys_pattern: Option<String>,
}
impl From<PublicKeyAuthConfigSerde> for PublicKeyAuthConfig {
fn from(serde_config: PublicKeyAuthConfigSerde) -> Self {
if let Some(dir) = serde_config.authorized_keys_dir {
PublicKeyAuthConfig::with_directory(dir)
} else if let Some(pattern) = serde_config.authorized_keys_pattern {
PublicKeyAuthConfig::with_pattern(pattern)
} else {
PublicKeyAuthConfig::default()
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PasswordAuthConfigSerde {
pub users_file: Option<PathBuf>,
#[serde(default)]
pub users: Vec<UserDefinition>,
}
impl From<PasswordAuthConfigSerde> for PasswordAuthConfig {
fn from(serde_config: PasswordAuthConfigSerde) -> Self {
PasswordAuthConfig {
users_file: serde_config.users_file,
users: serde_config.users,
}
}
}
fn default_listen_address() -> String {
"0.0.0.0:2222".to_string()
}
fn default_max_connections() -> usize {
100
}
fn default_max_auth_attempts() -> u32 {
5
}
fn default_auth_timeout_secs() -> u64 {
120
}
fn default_idle_timeout_secs() -> u64 {
0 }
fn default_auth_window_secs() -> u64 {
300 }
fn default_ban_time_secs() -> u64 {
300 }
fn default_true() -> bool {
true
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host_keys: Vec::new(),
listen_address: default_listen_address(),
max_connections: default_max_connections(),
max_auth_attempts: default_max_auth_attempts(),
auth_timeout_secs: default_auth_timeout_secs(),
idle_timeout_secs: default_idle_timeout_secs(),
allow_password_auth: false,
allow_publickey_auth: true,
allow_keyboard_interactive: false,
banner: None,
publickey_auth: PublicKeyAuthConfigSerde::default(),
password_auth: PasswordAuthConfigSerde::default(),
exec: ExecConfig::default(),
scp_enabled: true,
auth_window_secs: default_auth_window_secs(),
ban_time_secs: default_ban_time_secs(),
whitelist_ips: Vec::new(),
allowed_ips: Vec::new(),
blocked_ips: Vec::new(),
max_sessions_per_user: default_max_sessions_per_user(),
session_timeout_secs: 0,
}
}
}
impl ServerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn builder() -> ServerConfigBuilder {
ServerConfigBuilder::default()
}
pub fn auth_timeout(&self) -> Duration {
Duration::from_secs(self.auth_timeout_secs)
}
pub fn idle_timeout(&self) -> Option<Duration> {
if self.idle_timeout_secs == 0 {
None
} else {
Some(Duration::from_secs(self.idle_timeout_secs))
}
}
pub fn session_timeout(&self) -> Option<Duration> {
if self.session_timeout_secs == 0 {
None
} else {
Some(Duration::from_secs(self.session_timeout_secs))
}
}
pub fn session_config(&self) -> super::session::SessionConfig {
let mut config = super::session::SessionConfig::new()
.with_max_sessions_per_user(self.max_sessions_per_user)
.with_max_total_sessions(self.max_connections);
if self.idle_timeout_secs > 0 {
config = config.with_idle_timeout(Duration::from_secs(self.idle_timeout_secs));
}
if self.session_timeout_secs > 0 {
config = config.with_session_timeout(Duration::from_secs(self.session_timeout_secs));
}
config
}
pub fn has_host_keys(&self) -> bool {
!self.host_keys.is_empty()
}
pub fn add_host_key(&mut self, path: impl Into<PathBuf>) {
self.host_keys.push(path.into());
}
pub fn create_auth_provider(&self) -> Arc<dyn AuthProvider> {
if !self.allow_publickey_auth && !self.allow_password_auth {
let config: PublicKeyAuthConfig = self.publickey_auth.clone().into();
return Arc::new(PublicKeyVerifier::new(config));
}
if self.allow_publickey_auth && !self.allow_password_auth {
let config: PublicKeyAuthConfig = self.publickey_auth.clone().into();
return Arc::new(PublicKeyVerifier::new(config));
}
let publickey_config = if self.allow_publickey_auth {
Some(self.publickey_auth.clone().into())
} else {
None
};
let password_config = if self.allow_password_auth {
Some(self.password_auth.clone().into())
} else {
None
};
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
match tokio::task::block_in_place(|| {
handle.block_on(CompositeAuthProvider::new(
publickey_config,
password_config,
))
}) {
Ok(provider) => Arc::new(provider),
Err(e) => {
tracing::error!(error = %e, "Failed to create composite auth provider, falling back to publickey only");
let config: PublicKeyAuthConfig = self.publickey_auth.clone().into();
Arc::new(PublicKeyVerifier::new(config))
}
}
}
Err(_) => {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
match rt.block_on(CompositeAuthProvider::new(
publickey_config,
password_config,
)) {
Ok(provider) => Arc::new(provider),
Err(e) => {
tracing::error!(error = %e, "Failed to create composite auth provider, falling back to publickey only");
let config: PublicKeyAuthConfig = self.publickey_auth.clone().into();
Arc::new(PublicKeyVerifier::new(config))
}
}
}
}
}
}
#[derive(Debug, Default)]
pub struct ServerConfigBuilder {
config: ServerConfig,
}
impl ServerConfigBuilder {
pub fn host_keys(mut self, keys: Vec<PathBuf>) -> Self {
self.config.host_keys = keys;
self
}
pub fn host_key(mut self, key: impl Into<PathBuf>) -> Self {
self.config.host_keys.push(key.into());
self
}
pub fn listen_address(mut self, addr: impl Into<String>) -> Self {
self.config.listen_address = addr.into();
self
}
pub fn max_connections(mut self, max: usize) -> Self {
self.config.max_connections = max;
self
}
pub fn max_auth_attempts(mut self, max: u32) -> Self {
self.config.max_auth_attempts = max;
self
}
pub fn auth_timeout_secs(mut self, secs: u64) -> Self {
self.config.auth_timeout_secs = secs;
self
}
pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
self.config.idle_timeout_secs = secs;
self
}
pub fn allow_password_auth(mut self, allow: bool) -> Self {
self.config.allow_password_auth = allow;
self
}
pub fn allow_publickey_auth(mut self, allow: bool) -> Self {
self.config.allow_publickey_auth = allow;
self
}
pub fn allow_keyboard_interactive(mut self, allow: bool) -> Self {
self.config.allow_keyboard_interactive = allow;
self
}
pub fn banner(mut self, banner: impl Into<String>) -> Self {
self.config.banner = Some(banner.into());
self
}
pub fn authorized_keys_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.config.publickey_auth.authorized_keys_dir = Some(dir.into());
self.config.publickey_auth.authorized_keys_pattern = None;
self
}
pub fn authorized_keys_pattern(mut self, pattern: impl Into<String>) -> Self {
self.config.publickey_auth.authorized_keys_pattern = Some(pattern.into());
self.config.publickey_auth.authorized_keys_dir = None;
self
}
pub fn password_users_file(mut self, path: impl Into<PathBuf>) -> Self {
self.config.password_auth.users_file = Some(path.into());
self
}
pub fn password_users(mut self, users: Vec<UserDefinition>) -> Self {
self.config.password_auth.users = users;
self
}
pub fn exec(mut self, exec_config: ExecConfig) -> Self {
self.config.exec = exec_config;
self
}
pub fn exec_timeout_secs(mut self, secs: u64) -> Self {
self.config.exec.timeout_secs = secs;
self
}
pub fn exec_shell(mut self, shell: impl Into<PathBuf>) -> Self {
self.config.exec.default_shell = shell.into();
self
}
pub fn scp_enabled(mut self, enabled: bool) -> Self {
self.config.scp_enabled = enabled;
self
}
pub fn max_sessions_per_user(mut self, max: usize) -> Self {
self.config.max_sessions_per_user = max;
self
}
pub fn session_timeout_secs(mut self, secs: u64) -> Self {
self.config.session_timeout_secs = secs;
self
}
pub fn build(self) -> ServerConfig {
self.config
}
}
impl ServerFileConfig {
pub fn into_server_config(self) -> ServerConfig {
let listen_address = format!("{}:{}", self.server.bind_address, self.server.port);
let allow_publickey = self.auth.methods.contains(&AuthMethod::PublicKey);
let allow_password = self.auth.methods.contains(&AuthMethod::Password);
ServerConfig {
host_keys: self.server.host_keys,
listen_address,
max_connections: self.server.max_connections,
max_auth_attempts: self.security.max_auth_attempts,
auth_timeout_secs: self.server.timeout,
idle_timeout_secs: self.security.idle_timeout,
allow_password_auth: allow_password,
allow_publickey_auth: allow_publickey,
allow_keyboard_interactive: false,
banner: None,
publickey_auth: PublicKeyAuthConfigSerde {
authorized_keys_dir: self.auth.publickey.authorized_keys_dir,
authorized_keys_pattern: self.auth.publickey.authorized_keys_pattern,
},
password_auth: PasswordAuthConfigSerde {
users_file: self.auth.password.users_file,
users: self.auth.password.users,
},
exec: ExecConfig {
default_shell: self.shell.default,
timeout_secs: self.shell.command_timeout,
env: self.shell.env,
working_dir: None,
allowed_commands: None,
blocked_commands: Vec::new(),
},
scp_enabled: self.scp.enabled,
auth_window_secs: self.security.auth_window,
ban_time_secs: self.security.ban_time,
whitelist_ips: self.security.whitelist_ips,
allowed_ips: self.security.allowed_ips,
blocked_ips: self.security.blocked_ips,
max_sessions_per_user: self.security.max_sessions_per_user,
session_timeout_secs: self.security.session_timeout,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ServerConfig::default();
assert!(config.host_keys.is_empty());
assert_eq!(config.listen_address, "0.0.0.0:2222");
assert_eq!(config.max_connections, 100);
assert_eq!(config.max_auth_attempts, 5);
assert!(!config.allow_password_auth);
assert!(config.allow_publickey_auth);
assert!(config.scp_enabled);
}
#[test]
fn test_config_builder() {
let config = ServerConfig::builder()
.host_key("/etc/ssh/ssh_host_ed25519_key")
.listen_address("127.0.0.1:22")
.max_connections(50)
.max_auth_attempts(3)
.allow_password_auth(true)
.banner("Welcome to bssh server!")
.build();
assert_eq!(config.host_keys.len(), 1);
assert_eq!(config.listen_address, "127.0.0.1:22");
assert_eq!(config.max_connections, 50);
assert_eq!(config.max_auth_attempts, 3);
assert!(config.allow_password_auth);
assert_eq!(config.banner, Some("Welcome to bssh server!".to_string()));
}
#[test]
fn test_file_config_to_server_config_conversion() {
let mut file_config = ServerFileConfig::default();
file_config.server.bind_address = "127.0.0.1".to_string();
file_config.server.port = 2223;
file_config.server.host_keys = vec![PathBuf::from("/test/key")];
file_config.auth.methods = vec![AuthMethod::PublicKey, AuthMethod::Password];
file_config.security.max_auth_attempts = 3;
file_config.security.idle_timeout = 600;
let server_config = file_config.into_server_config();
assert_eq!(server_config.listen_address, "127.0.0.1:2223");
assert_eq!(server_config.host_keys.len(), 1);
assert_eq!(server_config.max_auth_attempts, 3);
assert_eq!(server_config.idle_timeout_secs, 600);
assert!(server_config.allow_publickey_auth);
assert!(server_config.allow_password_auth);
}
#[test]
fn test_config_new() {
let config = ServerConfig::new();
assert!(config.host_keys.is_empty());
assert_eq!(config.listen_address, "0.0.0.0:2222");
}
#[test]
fn test_auth_timeout() {
let config = ServerConfig::default();
assert_eq!(config.auth_timeout(), Duration::from_secs(120));
}
#[test]
fn test_idle_timeout() {
let mut config = ServerConfig::default();
assert!(config.idle_timeout().is_none());
config.idle_timeout_secs = 300;
assert_eq!(config.idle_timeout(), Some(Duration::from_secs(300)));
}
#[test]
fn test_has_host_keys() {
let mut config = ServerConfig::default();
assert!(!config.has_host_keys());
config.add_host_key("/path/to/key");
assert!(config.has_host_keys());
}
}