refactor(utils): decouple config deps and move sys helpers (#2520)

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
This commit is contained in:
houseme
2026-04-13 21:05:03 +08:00
committed by GitHub
parent 505a566c7c
commit 979626c370
17 changed files with 218 additions and 183 deletions
+168 -56
View File
@@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::get_env_bool;
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustls::RootCertStore;
use rustls::server::{
ClientHello, ResolvesServerCert, ResolvesServerCertUsingSni, WebPkiClientVerifier, danger::ClientCertVerifier,
@@ -22,11 +20,139 @@ use rustls::sign::CertifiedKey;
use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use std::collections::HashMap;
use std::io::Error;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::{fs, io};
use tracing::{debug, warn};
/// Options for loading certificate/key pairs from a directory tree.
#[derive(Debug, Clone)]
pub struct CertDirectoryLoadOptions {
dir_path: PathBuf,
cert_filename: String,
key_filename: String,
}
impl CertDirectoryLoadOptions {
/// Create a builder with explicit certificate and private key filenames.
pub fn builder(
dir_path: impl Into<PathBuf>,
cert_filename: impl Into<String>,
key_filename: impl Into<String>,
) -> CertDirectoryLoadOptionsBuilder {
CertDirectoryLoadOptionsBuilder {
dir_path: dir_path.into(),
cert_filename: cert_filename.into(),
key_filename: key_filename.into(),
}
}
fn validate(&self) -> io::Result<()> {
if self.cert_filename.is_empty() {
return Err(certs_error("certificate filename cannot be empty".to_string()));
}
if self.key_filename.is_empty() {
return Err(certs_error("private key filename cannot be empty".to_string()));
}
Ok(())
}
}
/// Builder for [`CertDirectoryLoadOptions`].
#[derive(Debug, Clone)]
pub struct CertDirectoryLoadOptionsBuilder {
dir_path: PathBuf,
cert_filename: String,
key_filename: String,
}
impl CertDirectoryLoadOptionsBuilder {
/// Override the certificate filename searched in the directory.
pub fn cert_filename(mut self, cert_filename: impl Into<String>) -> Self {
self.cert_filename = cert_filename.into();
self
}
/// Override the private key filename searched in the directory.
pub fn key_filename(mut self, key_filename: impl Into<String>) -> Self {
self.key_filename = key_filename.into();
self
}
/// Build the load options value.
pub fn build(self) -> CertDirectoryLoadOptions {
CertDirectoryLoadOptions {
dir_path: self.dir_path,
cert_filename: self.cert_filename,
key_filename: self.key_filename,
}
}
}
/// Options for building an mTLS WebPki client verifier.
#[derive(Debug, Clone)]
pub struct WebPkiClientVerifierOptions {
tls_path: PathBuf,
enabled: bool,
client_ca_cert_filename: String,
fallback_ca_cert_filename: String,
}
impl WebPkiClientVerifierOptions {
/// Create a builder with explicit CA bundle filenames.
pub fn builder(
tls_path: impl Into<PathBuf>,
client_ca_cert_filename: impl Into<String>,
fallback_ca_cert_filename: impl Into<String>,
) -> WebPkiClientVerifierOptionsBuilder {
WebPkiClientVerifierOptionsBuilder {
tls_path: tls_path.into(),
enabled: false,
client_ca_cert_filename: client_ca_cert_filename.into(),
fallback_ca_cert_filename: fallback_ca_cert_filename.into(),
}
}
}
/// Builder for [`WebPkiClientVerifierOptions`].
#[derive(Debug, Clone)]
pub struct WebPkiClientVerifierOptionsBuilder {
tls_path: PathBuf,
enabled: bool,
client_ca_cert_filename: String,
fallback_ca_cert_filename: String,
}
impl WebPkiClientVerifierOptionsBuilder {
/// Set whether mTLS verification should be enabled.
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
/// Override the preferred client CA bundle filename.
pub fn client_ca_cert_filename(mut self, client_ca_cert_filename: impl Into<String>) -> Self {
self.client_ca_cert_filename = client_ca_cert_filename.into();
self
}
/// Override the fallback CA bundle filename.
pub fn fallback_ca_cert_filename(mut self, fallback_ca_cert_filename: impl Into<String>) -> Self {
self.fallback_ca_cert_filename = fallback_ca_cert_filename.into();
self
}
/// Build the verifier options value.
pub fn build(self) -> WebPkiClientVerifierOptions {
WebPkiClientVerifierOptions {
tls_path: self.tls_path,
enabled: self.enabled,
client_ca_cert_filename: self.client_ca_cert_filename,
fallback_ca_cert_filename: self.fallback_ca_cert_filename,
}
}
}
/// Load public certificate from file.
/// This function loads a public certificate from the specified file.
///
@@ -72,24 +198,28 @@ pub fn load_cert_bundle_der_bytes(path: &str) -> io::Result<Vec<Vec<u8>>> {
Ok(certs.into_iter().map(|c| c.to_vec()).collect())
}
/// Builds a WebPkiClientVerifier for mTLS if enabled via environment variable.
/// Builds a WebPkiClientVerifier for mTLS when enabled by the caller.
///
/// # Arguments
/// * `tls_path` - Directory containing client CA certificates
/// * `options` - mTLS verifier options, including the TLS directory and CA bundle filenames
///
/// # Returns
/// * `Ok(Some(verifier))` if mTLS is enabled and CA certs are found
/// * `Ok(None)` if mTLS is disabled
/// * `Err` if mTLS is enabled but configuration is invalid
pub fn build_webpki_client_verifier(tls_path: &str) -> io::Result<Option<Arc<dyn ClientCertVerifier>>> {
if !get_env_bool(rustfs_config::ENV_SERVER_MTLS_ENABLE, rustfs_config::DEFAULT_SERVER_MTLS_ENABLE) {
pub fn build_webpki_client_verifier(options: WebPkiClientVerifierOptions) -> io::Result<Option<Arc<dyn ClientCertVerifier>>> {
if !options.enabled {
return Ok(None);
}
let ca_path = mtls_ca_bundle_path(tls_path).ok_or_else(|| {
let tls_path = &options.tls_path;
let ca_path = mtls_ca_bundle_path(&options).ok_or_else(|| {
Error::other(format!(
"RUSTFS_SERVER_MTLS_ENABLE=true but missing {}/client_ca.crt (or fallback {}/ca.crt)",
tls_path, tls_path
"mTLS is enabled but missing {}/{} (or fallback {}/{})",
tls_path.display(),
options.client_ca_cert_filename,
tls_path.display(),
options.fallback_ca_cert_filename
))
})?;
@@ -114,14 +244,12 @@ pub fn build_webpki_client_verifier(tls_path: &str) -> io::Result<Option<Arc<dyn
}
/// Locate the mTLS client CA bundle in the specified TLS path
fn mtls_ca_bundle_path(tls_path: &str) -> Option<std::path::PathBuf> {
use std::path::Path;
let p1 = Path::new(tls_path).join(rustfs_config::RUSTFS_CLIENT_CA_CERT_FILENAME);
fn mtls_ca_bundle_path(options: &WebPkiClientVerifierOptions) -> Option<PathBuf> {
let p1 = options.tls_path.join(&options.client_ca_cert_filename);
if p1.exists() {
return Some(p1);
}
let p2 = Path::new(tls_path).join(rustfs_config::RUSTFS_CA_CERT);
let p2 = options.tls_path.join(&options.fallback_ca_cert_filename);
if p2.exists() {
return Some(p2);
}
@@ -162,30 +290,33 @@ pub fn certs_error(err: String) -> Error {
/// Load all certificates and private keys in the directory
/// This function loads all certificate and private key pairs from the specified directory.
/// It looks for files named `rustfs_cert.pem` and `rustfs_key.pem` in each subdirectory.
/// It looks for files named `options.cert_filename` and `options.key_filename` in each subdirectory.
/// The root directory can also contain a default certificate/private key pair.
///
/// # Arguments
/// * `dir_path` - A string slice that holds the path to the directory containing the certificates and private keys.
/// * `options` - Directory and filename options for discovering certificates and private keys.
///
/// # Returns
/// * An io::Result containing a HashMap where the keys are domain names (or "default" for the root certificate) and the values are tuples of (Vec<CertificateDer>, PrivateKeyDer). If no valid certificate/private key pairs are found, an io::Error is returned.
///
pub fn load_all_certs_from_directory(
dir_path: &str,
options: CertDirectoryLoadOptions,
) -> io::Result<HashMap<String, (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>> {
options.validate()?;
let mut cert_key_pairs = HashMap::new();
let dir = Path::new(dir_path);
let dir = options.dir_path.as_path();
if !dir.exists() || !dir.is_dir() {
return Err(certs_error(format!(
"The certificate directory does not exist or is not a directory: {dir_path}"
"The certificate directory does not exist or is not a directory: {}",
dir.display()
)));
}
// 1. First check whether there is a certificate/private key pair in the root directory
let root_cert_path = dir.join(RUSTFS_TLS_CERT);
let root_key_path = dir.join(RUSTFS_TLS_KEY);
let root_cert_path = dir.join(&options.cert_filename);
let root_key_path = dir.join(&options.key_filename);
if root_cert_path.exists() && root_key_path.exists() {
debug!("find the root directory certificate: {:?}", root_cert_path);
@@ -218,8 +349,8 @@ pub fn load_all_certs_from_directory(
.ok_or_else(|| certs_error(format!("invalid domain name directory:{path:?}")))?;
// find certificate and private key files
let cert_path = path.join(RUSTFS_TLS_CERT); // e.g., rustfs_cert.pem
let key_path = path.join(RUSTFS_TLS_KEY); // e.g., rustfs_key.pem
let cert_path = path.join(&options.cert_filename); // e.g., rustfs_cert.pem
let key_path = path.join(&options.key_filename); // e.g., rustfs_key.pem
if cert_path.exists() && key_path.exists() {
debug!("find the domain name certificate: {} in {:?}", domain_name, cert_path);
@@ -253,7 +384,8 @@ pub fn load_all_certs_from_directory(
if cert_key_pairs.is_empty() {
return Err(certs_error(format!(
"No valid certificate/private key pair found in directory {dir_path}"
"No valid certificate/private key pair found in directory {}",
dir.display()
)));
}
@@ -334,15 +466,6 @@ pub fn create_multi_cert_resolver(
})
}
/// Checks if TLS key logging is enabled.
///
/// # Returns
/// * A boolean indicating whether TLS key logging is enabled based on the `RUSTFS_TLS_KEYLOG` environment variable.
///
pub fn tls_key_log() -> bool {
get_env_bool(rustfs_config::ENV_TLS_KEYLOG, rustfs_config::DEFAULT_TLS_KEYLOG)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -350,6 +473,10 @@ mod tests {
use std::io::ErrorKind;
use tempfile::TempDir;
fn default_load_options(path: impl Into<PathBuf>) -> CertDirectoryLoadOptions {
CertDirectoryLoadOptions::builder(path, "rustfs_cert.pem", "rustfs_key.pem").build()
}
#[test]
fn test_certs_error_function() {
let error_msg = "Test error message";
@@ -433,7 +560,7 @@ mod tests {
#[test]
fn test_load_all_certs_from_directory_not_exists() {
let result = load_all_certs_from_directory("/non/existent/directory");
let result = load_all_certs_from_directory(default_load_options("/non/existent/directory"));
assert!(result.is_err());
let error = result.unwrap_err();
@@ -444,7 +571,7 @@ mod tests {
fn test_load_all_certs_from_directory_empty() {
let temp_dir = TempDir::new().unwrap();
let result = load_all_certs_from_directory(temp_dir.path().to_str().unwrap());
let result = load_all_certs_from_directory(default_load_options(temp_dir.path()));
assert!(result.is_err());
let error = result.unwrap_err();
@@ -457,7 +584,7 @@ mod tests {
let file_path = temp_dir.path().join("not_a_directory.txt");
fs::write(&file_path, "content").unwrap();
let result = load_all_certs_from_directory(file_path.to_str().unwrap());
let result = load_all_certs_from_directory(default_load_options(&file_path));
assert!(result.is_err());
let error = result.unwrap_err();
@@ -523,27 +650,12 @@ mod tests {
];
for path in path_cases {
let result = load_all_certs_from_directory(path);
let result = load_all_certs_from_directory(default_load_options(path));
// All should fail since these are not valid cert directories
assert!(result.is_err());
}
}
#[test]
fn test_filename_constants_consistency() {
// Test that the constants match expected values
assert_eq!(RUSTFS_TLS_CERT, "rustfs_cert.pem");
assert_eq!(RUSTFS_TLS_KEY, "rustfs_key.pem");
// Test that constants are not empty
assert!(!RUSTFS_TLS_CERT.is_empty());
assert!(!RUSTFS_TLS_KEY.is_empty());
// Test that constants have proper extensions
assert!(RUSTFS_TLS_CERT.ends_with(".pem"));
assert!(RUSTFS_TLS_KEY.ends_with(".pem"));
}
#[test]
fn test_directory_structure_validation() {
let temp_dir = TempDir::new().unwrap();
@@ -553,7 +665,7 @@ mod tests {
fs::create_dir(&sub_dir).unwrap();
// Should fail because no certificates found
let result = load_all_certs_from_directory(temp_dir.path().to_str().unwrap());
let result = load_all_certs_from_directory(default_load_options(temp_dir.path()));
assert!(result.is_err());
assert!(
result
@@ -571,7 +683,7 @@ mod tests {
let unicode_dir = temp_dir.path().join("test_directory");
fs::create_dir(&unicode_dir).unwrap();
let result = load_all_certs_from_directory(unicode_dir.to_str().unwrap());
let result = load_all_certs_from_directory(default_load_options(&unicode_dir));
assert!(result.is_err());
assert!(
result
@@ -593,7 +705,7 @@ mod tests {
.map(|_| {
let path = Arc::clone(&dir_path);
thread::spawn(move || {
let result = load_all_certs_from_directory(&path);
let result = load_all_certs_from_directory(default_load_options(path.as_str()));
// All should fail since directory is empty
assert!(result.is_err());
})
-3
View File
@@ -238,9 +238,6 @@ mod tests {
use std::time::Instant;
let data = vec![42u8; 1024 * 100]; // 100KB of repetitive data
// let mut data = vec![0u8; 1024 * 1024];
// rand::thread_rng().fill(&mut data[..]);
let start = Instant::now();
let mut times = Vec::new();
-94
View File
@@ -12,9 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use rustfs_config::{DEFAULT_LOG_DIR, DEFAULT_LOG_FILENAME};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::debug;
@@ -61,98 +59,6 @@ pub fn get_project_root() -> Result<PathBuf, String> {
Err("The project root directory cannot be obtained. Please check the running environment and project structure.".to_string())
}
/// Get the log directory as a string
/// This function will try to find a writable log directory in the following order:
///
/// 1. Environment variables are specified
/// 2. System temporary directory
/// 3. User home directory
/// 4. Current working directory
/// 5. Relative path
///
/// # Arguments
/// * `key` - The environment variable key to check for log directory
///
/// # Returns
/// * `String` - The log directory path as a string
///
pub fn get_log_directory_to_string(key: &str) -> String {
get_log_directory(key).to_string_lossy().to_string()
}
/// Get the log directory
/// This function will try to find a writable log directory in the following order:
///
/// 1. Environment variables are specified
/// 2. System temporary directory
/// 3. User home directory
/// 4. Current working directory
/// 5. Relative path
///
/// # Arguments
/// * `key` - The environment variable key to check for log directory
///
/// # Returns
/// * `PathBuf` - The log directory path
///
pub fn get_log_directory(key: &str) -> PathBuf {
// Environment variables are specified
if let Ok(log_dir) = env::var(key) {
let path = PathBuf::from(log_dir);
if ensure_directory_writable(&path) {
return path;
}
}
// System temporary directory
if let Ok(mut temp_dir) = env::temp_dir().canonicalize() {
temp_dir.push(DEFAULT_LOG_FILENAME);
temp_dir.push(DEFAULT_LOG_DIR);
if ensure_directory_writable(&temp_dir) {
return temp_dir;
}
}
// User home directory
if let Ok(home_dir) = env::var("HOME").or_else(|_| env::var("USERPROFILE")) {
let mut path = PathBuf::from(home_dir);
path.push(format!(".{DEFAULT_LOG_FILENAME}"));
path.push(DEFAULT_LOG_DIR);
if ensure_directory_writable(&path) {
return path;
}
}
// Current working directory
if let Ok(current_dir) = env::current_dir() {
let mut path = current_dir;
path.push(DEFAULT_LOG_DIR);
if ensure_directory_writable(&path) {
return path;
}
}
// Relative path
PathBuf::from(DEFAULT_LOG_DIR)
}
fn ensure_directory_writable(path: &PathBuf) -> bool {
// Try creating a catalog
if fs::create_dir_all(path).is_err() {
return false;
}
// Check write permissions
let test_file = path.join(".write_test");
match fs::write(&test_file, "test") {
Ok(_) => {
let _ = fs::remove_file(&test_file);
true
}
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
-6
View File
@@ -73,12 +73,6 @@ pub use compress::*;
#[cfg(feature = "notify")]
mod notify;
#[cfg(feature = "sys")]
pub mod sys;
#[cfg(feature = "sys")]
pub use sys::user_agent::*;
#[cfg(feature = "notify")]
pub use notify::*;
-15
View File
@@ -1,15 +0,0 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub(crate) mod user_agent;
-206
View File
@@ -1,206 +0,0 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use rustfs_config::VERSION;
use std::borrow::Cow;
use std::env;
use std::fmt;
use std::sync::OnceLock;
#[cfg(not(target_os = "openbsd"))]
use sysinfo::System;
/// Business Type Enumeration
#[derive(Debug, Clone, PartialEq)]
pub enum ServiceType {
Basis,
Core,
Event,
Logger,
Custom(Cow<'static, str>),
}
impl ServiceType {
fn as_str(&self) -> &str {
match self {
ServiceType::Basis => "basis",
ServiceType::Core => "core",
ServiceType::Event => "event",
ServiceType::Logger => "logger",
ServiceType::Custom(s) => s,
}
}
}
/// UserAgent structure to hold User-Agent information
/// including OS platform, architecture, version, and service type.
#[derive(Debug)]
struct UserAgent {
os_platform: &'static str,
arch: &'static str,
version: &'static str,
service: ServiceType,
}
static OS_PLATFORM: OnceLock<String> = OnceLock::new();
impl UserAgent {
/// Create a new UserAgent instance and accept business type parameters
fn new(service: ServiceType) -> Self {
UserAgent {
os_platform: Self::get_os_platform(),
arch: env::consts::ARCH,
version: VERSION,
service,
}
}
/// Obtain operating system platform information using a thread-safe cache.
///
/// The value is computed once on first use via `OnceLock` and then reused
/// for all subsequent calls for the lifetime of the program.
fn get_os_platform() -> &'static str {
OS_PLATFORM.get_or_init(|| {
if cfg!(target_os = "windows") {
Self::get_windows_platform()
} else if cfg!(target_os = "macos") {
Self::get_macos_platform()
} else if cfg!(target_os = "linux") {
Self::get_linux_platform()
} else if cfg!(target_os = "freebsd") {
Self::get_freebsd_platform()
} else if cfg!(target_os = "netbsd") {
Self::get_netbsd_platform()
} else {
"Unknown".to_string()
}
})
}
/// Get Windows platform information
#[cfg(windows)]
fn get_windows_platform() -> String {
let version = System::os_version().unwrap_or_else(|| "NT Unknown".to_string());
if version.starts_with("Windows") {
version
} else {
format!("Windows NT {version}")
}
}
#[cfg(not(windows))]
fn get_windows_platform() -> String {
"N/A".to_string()
}
/// Get macOS platform information
#[cfg(target_os = "macos")]
fn get_macos_platform() -> String {
let version_str = System::os_version().unwrap_or_else(|| "14.0.0".to_string());
let mut parts = version_str.split('.');
let major = parts.next().unwrap_or("14");
let minor = parts.next().unwrap_or("0");
let patch = parts.next().unwrap_or("0");
let cpu_info = if env::consts::ARCH == "aarch64" { "Apple" } else { "Intel" };
format!("Macintosh; {cpu_info} Mac OS X {major}_{minor}_{patch}")
}
#[cfg(not(target_os = "macos"))]
fn get_macos_platform() -> String {
"N/A".to_string()
}
/// Get Linux platform information
#[cfg(target_os = "linux")]
fn get_linux_platform() -> String {
let os_name = System::long_os_version().unwrap_or_else(|| "Linux Unknown".to_string());
format!("X11; {os_name}")
}
#[cfg(not(target_os = "linux"))]
fn get_linux_platform() -> String {
"N/A".to_string()
}
#[cfg(target_os = "freebsd")]
fn get_freebsd_platform() -> String {
format!("FreeBSD; {}", env::consts::ARCH)
}
#[cfg(not(target_os = "freebsd"))]
fn get_freebsd_platform() -> String {
"N/A".to_string()
}
#[cfg(target_os = "netbsd")]
fn get_netbsd_platform() -> String {
format!("NetBSD; {}", env::consts::ARCH)
}
#[cfg(not(target_os = "netbsd"))]
fn get_netbsd_platform() -> String {
"N/A".to_string()
}
}
impl fmt::Display for UserAgent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Mozilla/5.0 ({}; {}) RustFS/{}", self.os_platform, self.arch, self.version)?;
if self.service != ServiceType::Basis {
write!(f, " ({})", self.service.as_str())?;
}
Ok(())
}
}
/// Get the User-Agent string and accept business type parameters
pub fn get_user_agent(service: ServiceType) -> String {
UserAgent::new(service).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use rustfs_config::VERSION;
#[test]
fn test_user_agent_format_basis() {
let ua = get_user_agent(ServiceType::Basis);
assert!(ua.starts_with("Mozilla/5.0"));
assert!(ua.contains(&format!("RustFS/{VERSION}")));
assert!(!ua.contains("(basis)"));
}
#[test]
fn test_user_agent_format_core() {
let ua = get_user_agent(ServiceType::Core);
assert!(ua.contains(&format!("RustFS/{VERSION} (core)")));
}
#[test]
fn test_user_agent_format_custom() {
let ua = get_user_agent(ServiceType::Custom("monitor".into()));
assert!(ua.contains(&format!("RustFS/{VERSION} (monitor)")));
}
#[test]
fn test_os_platform_caching() {
let ua1 = UserAgent::new(ServiceType::Basis);
let ua2 = UserAgent::new(ServiceType::Basis);
assert_eq!(ua1.os_platform, ua2.os_platform);
// Ensure they point to the same static memory
assert!(std::ptr::eq(ua1.os_platform.as_ptr(), ua2.os_platform.as_ptr()));
}
}