mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 13:16:28 +00:00
feat(tls): add inspect command for TLS layouts (#3092)
This commit is contained in:
@@ -20,7 +20,7 @@ use rustls::sign::CertifiedKey;
|
|||||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
|
use rustls_pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Error;
|
use std::io::Error;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fs, io};
|
use std::{fs, io};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@@ -224,10 +224,118 @@ pub fn certs_error(err: String) -> Error {
|
|||||||
Error::other(err)
|
Error::other(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum TlsCertPairStatus {
|
||||||
|
MissingBoth,
|
||||||
|
MissingCert,
|
||||||
|
MissingKey,
|
||||||
|
Valid,
|
||||||
|
Invalid { error: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TlsCertPairStatus {
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
matches!(self, Self::Valid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TlsCertPairInspection {
|
||||||
|
pub cert_path: PathBuf,
|
||||||
|
pub key_path: PathBuf,
|
||||||
|
pub status: TlsCertPairStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TlsCertPairInspection {
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.status.is_valid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TlsDomainInspection {
|
||||||
|
pub domain_name: String,
|
||||||
|
pub pair: TlsCertPairInspection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TlsDirectoryInspection {
|
||||||
|
pub directory: PathBuf,
|
||||||
|
pub canonical_directory: Option<PathBuf>,
|
||||||
|
pub root_pair: TlsCertPairInspection,
|
||||||
|
pub domain_pairs: Vec<TlsDomainInspection>,
|
||||||
|
pub skipped_directory_names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TlsDirectoryInspection {
|
||||||
|
pub fn valid_domain_names(&self) -> Vec<&str> {
|
||||||
|
self.domain_pairs
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.pair.is_valid())
|
||||||
|
.map(|entry| entry.domain_name.as_str())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_valid_root_pair(&self) -> bool {
|
||||||
|
self.root_pair.is_valid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_discoverable_cert_domain_dir(domain_name: &str) -> bool {
|
fn is_discoverable_cert_domain_dir(domain_name: &str) -> bool {
|
||||||
!domain_name.starts_with('.')
|
!domain_name.starts_with('.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inspect_cert_directory(options: CertDirectoryLoadOptions) -> io::Result<TlsDirectoryInspection> {
|
||||||
|
options.validate()?;
|
||||||
|
|
||||||
|
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.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_pair = inspect_cert_key_pair(dir, &options.cert_filename, &options.key_filename);
|
||||||
|
let canonical_directory = fs::canonicalize(dir).ok();
|
||||||
|
let mut domain_pairs = Vec::new();
|
||||||
|
let mut skipped_directory_names = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.ok_or_else(|| certs_error(format!("invalid domain name directory:{path:?}")))?;
|
||||||
|
if !is_discoverable_cert_domain_dir(domain_name) {
|
||||||
|
skipped_directory_names.push(domain_name.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
domain_pairs.push(TlsDomainInspection {
|
||||||
|
domain_name: domain_name.to_string(),
|
||||||
|
pair: inspect_cert_key_pair(&path, &options.cert_filename, &options.key_filename),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
domain_pairs.sort_by(|left, right| left.domain_name.cmp(&right.domain_name));
|
||||||
|
skipped_directory_names.sort();
|
||||||
|
|
||||||
|
Ok(TlsDirectoryInspection {
|
||||||
|
directory: dir.to_path_buf(),
|
||||||
|
canonical_directory,
|
||||||
|
root_pair,
|
||||||
|
domain_pairs,
|
||||||
|
skipped_directory_names,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_all_certs_from_directory(
|
pub fn load_all_certs_from_directory(
|
||||||
options: CertDirectoryLoadOptions,
|
options: CertDirectoryLoadOptions,
|
||||||
) -> io::Result<HashMap<String, (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>> {
|
) -> io::Result<HashMap<String, (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>> {
|
||||||
@@ -327,6 +435,42 @@ fn load_cert_key_pair(cert_path: &str, key_path: &str) -> io::Result<(Vec<Certif
|
|||||||
Ok((certs, key))
|
Ok((certs, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inspect_cert_key_pair(dir: &Path, cert_filename: &str, key_filename: &str) -> TlsCertPairInspection {
|
||||||
|
let cert_path = dir.join(cert_filename);
|
||||||
|
let key_path = dir.join(key_filename);
|
||||||
|
let cert_exists = cert_path.exists();
|
||||||
|
let key_exists = key_path.exists();
|
||||||
|
|
||||||
|
let status = match (cert_exists, key_exists) {
|
||||||
|
(false, false) => TlsCertPairStatus::MissingBoth,
|
||||||
|
(false, true) => TlsCertPairStatus::MissingCert,
|
||||||
|
(true, false) => TlsCertPairStatus::MissingKey,
|
||||||
|
(true, true) => match cert_key_pair_utf8_paths(&cert_path, &key_path) {
|
||||||
|
Ok((cert_path, key_path)) => match load_cert_key_pair(cert_path, key_path) {
|
||||||
|
Ok(_) => TlsCertPairStatus::Valid,
|
||||||
|
Err(err) => TlsCertPairStatus::Invalid { error: err.to_string() },
|
||||||
|
},
|
||||||
|
Err(err) => TlsCertPairStatus::Invalid { error: err.to_string() },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
TlsCertPairInspection {
|
||||||
|
cert_path,
|
||||||
|
key_path,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cert_key_pair_utf8_paths<'a>(cert_path: &'a Path, key_path: &'a Path) -> io::Result<(&'a str, &'a str)> {
|
||||||
|
let cert_path = cert_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| certs_error(format!("Invalid UTF-8 in certificate path: {cert_path:?}")))?;
|
||||||
|
let key_path = key_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| certs_error(format!("Invalid UTF-8 in key path: {key_path:?}")))?;
|
||||||
|
Ok((cert_path, key_path))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_multi_cert_resolver(
|
pub fn create_multi_cert_resolver(
|
||||||
cert_key_pairs: HashMap<String, (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
|
cert_key_pairs: HashMap<String, (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
|
||||||
) -> io::Result<impl ResolvesServerCert> {
|
) -> io::Result<impl ResolvesServerCert> {
|
||||||
@@ -448,4 +592,36 @@ mod tests {
|
|||||||
assert!(!certs.contains_key("..data"));
|
assert!(!certs.contains_key("..data"));
|
||||||
assert_eq!(certs.len(), 2);
|
assert_eq!(certs.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inspect_cert_directory_reports_valid_root_and_domain_pairs() {
|
||||||
|
let temp_dir = TempDir::new().expect("tempdir should create");
|
||||||
|
write_test_cert_pair(temp_dir.path());
|
||||||
|
|
||||||
|
let domain_dir = temp_dir.path().join("example.com");
|
||||||
|
fs::create_dir(&domain_dir).expect("domain dir should create");
|
||||||
|
write_test_cert_pair(&domain_dir);
|
||||||
|
|
||||||
|
let inspection = inspect_cert_directory(default_load_options(temp_dir.path())).expect("inspection should succeed");
|
||||||
|
assert!(inspection.has_valid_root_pair());
|
||||||
|
assert_eq!(inspection.valid_domain_names(), vec!["example.com"]);
|
||||||
|
assert_eq!(inspection.domain_pairs.len(), 1);
|
||||||
|
assert!(inspection.domain_pairs[0].pair.is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inspect_cert_directory_reports_invalid_and_missing_pairs() {
|
||||||
|
let temp_dir = TempDir::new().expect("tempdir should create");
|
||||||
|
fs::write(temp_dir.path().join("rustfs_cert.pem"), "invalid certificate").expect("invalid cert should write");
|
||||||
|
fs::write(temp_dir.path().join("rustfs_key.pem"), "invalid key").expect("invalid key should write");
|
||||||
|
|
||||||
|
let domain_dir = temp_dir.path().join("broken.example.com");
|
||||||
|
fs::create_dir(&domain_dir).expect("domain dir should create");
|
||||||
|
fs::write(domain_dir.join("rustfs_cert.pem"), "invalid certificate").expect("invalid cert should write");
|
||||||
|
|
||||||
|
let inspection = inspect_cert_directory(default_load_options(temp_dir.path())).expect("inspection should succeed");
|
||||||
|
assert!(matches!(inspection.root_pair.status, TlsCertPairStatus::Invalid { .. }));
|
||||||
|
assert_eq!(inspection.domain_pairs.len(), 1);
|
||||||
|
assert_eq!(inspection.domain_pairs[0].pair.status, TlsCertPairStatus::MissingKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ pub mod source;
|
|||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
pub use certs::{
|
pub use certs::{
|
||||||
CertDirectoryLoadOptions, WebPkiClientVerifierOptions, build_webpki_client_verifier, create_multi_cert_resolver,
|
CertDirectoryLoadOptions, TlsCertPairInspection, TlsCertPairStatus, TlsDirectoryInspection, TlsDomainInspection,
|
||||||
|
WebPkiClientVerifierOptions, build_webpki_client_verifier, create_multi_cert_resolver, inspect_cert_directory,
|
||||||
load_all_certs_from_directory, load_cert_bundle_der_bytes, load_certs, load_private_key,
|
load_all_certs_from_directory, load_cert_bundle_der_bytes, load_certs, load_private_key,
|
||||||
};
|
};
|
||||||
pub use config::{ReloadApplyHint, ReloadDetectMode, TlsReloadOptions};
|
pub use config::{ReloadApplyHint, ReloadDetectMode, TlsReloadOptions};
|
||||||
|
|||||||
@@ -16,9 +16,10 @@
|
|||||||
//!
|
//!
|
||||||
//! This module contains the command-line interface definitions including:
|
//! This module contains the command-line interface definitions including:
|
||||||
//! - `Cli`: Main CLI parser
|
//! - `Cli`: Main CLI parser
|
||||||
//! - `Commands`: Subcommands (Server, Info)
|
//! - `Commands`: Subcommands (Server, Info, Tls)
|
||||||
//! - `ServerOpts`: Server subcommand options
|
//! - `ServerOpts`: Server subcommand options
|
||||||
//! - `InfoOpts`: Info subcommand options
|
//! - `InfoOpts`: Info subcommand options
|
||||||
|
//! - `TlsOpts`: TLS diagnostic subcommand options
|
||||||
//! - `InfoType`: Information type enum
|
//! - `InfoType`: Information type enum
|
||||||
//! - `CommandResult`: Result of parsing command line arguments
|
//! - `CommandResult`: Result of parsing command line arguments
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ pub(super) const LONG_VERSION: &str = concat!(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/// Known subcommands. When the first arg matches one of these, it is treated as a subcommand.
|
/// Known subcommands. When the first arg matches one of these, it is treated as a subcommand.
|
||||||
pub const KNOWN_SUBCOMMANDS: &[&str] = &["server", "info"];
|
pub const KNOWN_SUBCOMMANDS: &[&str] = &["server", "info", "tls"];
|
||||||
|
|
||||||
/// Preprocess argv for legacy compatibility: `rustfs <volume>` and `rustfs --address ...` are
|
/// Preprocess argv for legacy compatibility: `rustfs <volume>` and `rustfs --address ...` are
|
||||||
/// treated as `rustfs server <volume>` and `rustfs server --address ...` respectively.
|
/// treated as `rustfs server <volume>` and `rustfs server --address ...` respectively.
|
||||||
@@ -102,6 +103,8 @@ pub enum Commands {
|
|||||||
Server(Box<ServerOpts>),
|
Server(Box<ServerOpts>),
|
||||||
/// Display system information
|
/// Display system information
|
||||||
Info(InfoOpts),
|
Info(InfoOpts),
|
||||||
|
/// Inspect TLS certificate directory layout and parsing status
|
||||||
|
Tls(TlsOpts),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information type to display
|
/// Information type to display
|
||||||
@@ -135,6 +138,28 @@ pub struct InfoOpts {
|
|||||||
pub info_type: Option<InfoType>,
|
pub info_type: Option<InfoType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TLS diagnostic subcommand options
|
||||||
|
#[derive(Args, Clone)]
|
||||||
|
pub struct TlsOpts {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: TlsCommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TLS diagnostic subcommands
|
||||||
|
#[derive(Subcommand, Clone)]
|
||||||
|
pub enum TlsCommands {
|
||||||
|
/// Inspect a TLS certificate directory
|
||||||
|
Inspect(TlsInspectOpts),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TLS inspect options
|
||||||
|
#[derive(Args, Clone)]
|
||||||
|
pub struct TlsInspectOpts {
|
||||||
|
/// TLS directory to inspect
|
||||||
|
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Server subcommand options
|
/// Server subcommand options
|
||||||
#[derive(Args, Clone)]
|
#[derive(Args, Clone)]
|
||||||
pub struct ServerOpts {
|
pub struct ServerOpts {
|
||||||
@@ -263,6 +288,8 @@ pub enum CommandResult {
|
|||||||
Server(Box<super::Config>),
|
Server(Box<super::Config>),
|
||||||
/// Info command with options
|
/// Info command with options
|
||||||
Info(InfoOpts),
|
Info(InfoOpts),
|
||||||
|
/// TLS command with options
|
||||||
|
Tls(TlsOpts),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create default ServerOpts from environment variables
|
/// Create default ServerOpts from environment variables
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(unsafe_op_in_unsafe_fn)]
|
#[allow(unsafe_op_in_unsafe_fn)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::config::{Config, Opt};
|
use crate::config::{CommandResult, Config, Opt, TlsCommands};
|
||||||
use rustfs_config::{DEFAULT_CONSOLE_ADDRESS, DEFAULT_CONSOLE_ENABLE, DEFAULT_OBS_ENDPOINT, RUSTFS_REGION};
|
use rustfs_config::{DEFAULT_CONSOLE_ADDRESS, DEFAULT_CONSOLE_ENABLE, DEFAULT_OBS_ENDPOINT, RUSTFS_REGION};
|
||||||
use rustfs_credentials::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY};
|
use rustfs_credentials::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY};
|
||||||
use rustfs_ecstore::disks_layout::DisksLayout;
|
use rustfs_ecstore::disks_layout::DisksLayout;
|
||||||
@@ -72,6 +72,20 @@ mod tests {
|
|||||||
assert_eq!(opt_legacy.console_address, opt_server.console_address);
|
assert_eq!(opt_legacy.console_address, opt_server.console_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_tls_inspect_subcommand_parses() {
|
||||||
|
let result =
|
||||||
|
Opt::parse_command(["rustfs", "tls", "inspect", "--path", "/tmp/certs"]).expect("tls inspect command should parse");
|
||||||
|
|
||||||
|
match result {
|
||||||
|
CommandResult::Tls(opts) => match opts.command {
|
||||||
|
TlsCommands::Inspect(inspect) => assert_eq!(inspect.path, "/tmp/certs"),
|
||||||
|
},
|
||||||
|
_ => panic!("expected TLS command result"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn test_default_console_configuration() {
|
fn test_default_console_configuration() {
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ mod config_test;
|
|||||||
|
|
||||||
// Re-export public types
|
// Re-export public types
|
||||||
pub use cli::{CommandResult, InfoOpts, InfoType};
|
pub use cli::{CommandResult, InfoOpts, InfoType};
|
||||||
|
pub use cli::{TlsCommands, TlsInspectOpts, TlsOpts};
|
||||||
pub use config_struct::Config;
|
pub use config_struct::Config;
|
||||||
pub use info::execute_info;
|
pub use info::execute_info;
|
||||||
pub use opt::Opt;
|
pub use opt::Opt;
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ impl Opt {
|
|||||||
// This should not happen in parse_from, as it's handled by parse_command
|
// This should not happen in parse_from, as it's handled by parse_command
|
||||||
panic!("Info command should be handled by parse_command");
|
panic!("Info command should be handled by parse_command");
|
||||||
}
|
}
|
||||||
|
Some(Commands::Tls(_)) => {
|
||||||
|
panic!("TLS command should be handled by parse_command");
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
// Default to server with empty volumes (will be filled from env)
|
// Default to server with empty volumes (will be filled from env)
|
||||||
Self::from_server_opts(default_server_opts())
|
Self::from_server_opts(default_server_opts())
|
||||||
@@ -129,6 +132,7 @@ impl Opt {
|
|||||||
};
|
};
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Some(Commands::Info(opts)) => Ok(CommandResult::Info(opts)),
|
Some(Commands::Info(opts)) => Ok(CommandResult::Info(opts)),
|
||||||
|
Some(Commands::Tls(opts)) => Ok(CommandResult::Tls(opts)),
|
||||||
Some(Commands::Server(opts)) => Self::server_command_result(Self::from_server_opts(*opts)),
|
Some(Commands::Server(opts)) => Self::server_command_result(Self::from_server_opts(*opts)),
|
||||||
None => {
|
None => {
|
||||||
// Default to server with empty volumes (will be filled from env)
|
// Default to server with empty volumes (will be filled from env)
|
||||||
@@ -157,7 +161,7 @@ impl Opt {
|
|||||||
let cli = Cli::try_parse_from(args)?;
|
let cli = Cli::try_parse_from(args)?;
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Some(Commands::Server(opts)) => Ok(Self::from_server_opts(*opts)),
|
Some(Commands::Server(opts)) => Ok(Self::from_server_opts(*opts)),
|
||||||
Some(Commands::Info(_)) => Err(clap::Error::new(clap::error::ErrorKind::DisplayHelp)),
|
Some(Commands::Info(_)) | Some(Commands::Tls(_)) => Err(clap::Error::new(clap::error::ErrorKind::DisplayHelp)),
|
||||||
None => {
|
None => {
|
||||||
// Default to server with empty volumes
|
// Default to server with empty volumes
|
||||||
Ok(Self::from_server_opts(default_server_opts()))
|
Ok(Self::from_server_opts(default_server_opts()))
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ pub mod protocols;
|
|||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod startup_fs_guard;
|
pub mod startup_fs_guard;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
pub mod tls;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -159,10 +159,15 @@ async fn async_main() -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let rustfs::config::CommandResult::Tls(opts) = &command_result {
|
||||||
|
rustfs::tls::execute_tls(opts)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Get config for server command
|
// Get config for server command
|
||||||
let config = match command_result {
|
let config = match command_result {
|
||||||
rustfs::config::CommandResult::Server(cfg) => cfg,
|
rustfs::config::CommandResult::Server(cfg) => cfg,
|
||||||
rustfs::config::CommandResult::Info(_) => unreachable!(),
|
rustfs::config::CommandResult::Info(_) | rustfs::config::CommandResult::Tls(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the global config snapshot for info command
|
// Initialize the global config snapshot for info command
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// 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 crate::config::{TlsCommands, TlsInspectOpts, TlsOpts};
|
||||||
|
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
|
||||||
|
use rustfs_tls_runtime::{CertDirectoryLoadOptions, TlsCertPairInspection, TlsCertPairStatus, TlsSource, inspect_cert_directory};
|
||||||
|
use std::io::{Error, Result};
|
||||||
|
|
||||||
|
pub fn execute_tls(opts: &TlsOpts) -> Result<()> {
|
||||||
|
match &opts.command {
|
||||||
|
TlsCommands::Inspect(inspect) => execute_tls_inspect(inspect),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_tls_inspect(opts: &TlsInspectOpts) -> Result<()> {
|
||||||
|
let tls_source = TlsSource::from_directory(opts.path.trim());
|
||||||
|
let tls_dir = tls_source.validate_directory().map_err(Error::other)?;
|
||||||
|
let inspection = inspect_cert_directory(CertDirectoryLoadOptions::builder(tls_dir, RUSTFS_TLS_CERT, RUSTFS_TLS_KEY).build())?;
|
||||||
|
|
||||||
|
println!("TLS directory inspection");
|
||||||
|
println!("path: {}", inspection.directory.display());
|
||||||
|
if let Some(canonical_directory) = &inspection.canonical_directory {
|
||||||
|
println!("canonical path: {}", canonical_directory.display());
|
||||||
|
}
|
||||||
|
println!("root pair: {}", format_pair_summary(&inspection.root_pair));
|
||||||
|
|
||||||
|
let valid_domains = inspection.valid_domain_names();
|
||||||
|
let runtime_mode = if !valid_domains.is_empty() {
|
||||||
|
"multi-cert/SNI"
|
||||||
|
} else if inspection.has_valid_root_pair() {
|
||||||
|
"single-cert/default"
|
||||||
|
} else {
|
||||||
|
"no-usable-server-cert"
|
||||||
|
};
|
||||||
|
println!("runtime mode: {}", runtime_mode);
|
||||||
|
println!("valid domain pairs: {}", valid_domains.len());
|
||||||
|
if !valid_domains.is_empty() {
|
||||||
|
println!("valid domains: {}", valid_domains.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inspection.skipped_directory_names.is_empty() {
|
||||||
|
println!("skipped internal directories: {}", inspection.skipped_directory_names.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if inspection.domain_pairs.is_empty() {
|
||||||
|
println!("domain pairs: none");
|
||||||
|
} else {
|
||||||
|
println!("domain pairs:");
|
||||||
|
for domain in &inspection.domain_pairs {
|
||||||
|
println!(" - {}: {}", domain.domain_name, format_pair_summary(&domain.pair));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_pair_summary(pair: &TlsCertPairInspection) -> String {
|
||||||
|
match &pair.status {
|
||||||
|
TlsCertPairStatus::MissingBoth => format!(
|
||||||
|
"missing cert/key (expected cert={}, key={})",
|
||||||
|
pair.cert_path.display(),
|
||||||
|
pair.key_path.display()
|
||||||
|
),
|
||||||
|
TlsCertPairStatus::MissingCert => format!(
|
||||||
|
"missing cert (expected cert={}, key={})",
|
||||||
|
pair.cert_path.display(),
|
||||||
|
pair.key_path.display()
|
||||||
|
),
|
||||||
|
TlsCertPairStatus::MissingKey => format!(
|
||||||
|
"missing key (expected cert={}, key={})",
|
||||||
|
pair.cert_path.display(),
|
||||||
|
pair.key_path.display()
|
||||||
|
),
|
||||||
|
TlsCertPairStatus::Valid => {
|
||||||
|
format!("valid cert/key pair (cert={}, key={})", pair.cert_path.display(), pair.key_path.display())
|
||||||
|
}
|
||||||
|
TlsCertPairStatus::Invalid { error } => format!(
|
||||||
|
"invalid cert/key pair (cert={}, key={}): {}",
|
||||||
|
pair.cert_path.display(),
|
||||||
|
pair.key_path.display(),
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn write_test_cert_pair(dir: &std::path::Path) {
|
||||||
|
let rcgen::CertifiedKey { cert, signing_key } =
|
||||||
|
rcgen::generate_simple_self_signed(vec!["example.com".to_string()]).expect("cert should generate");
|
||||||
|
fs::write(dir.join(RUSTFS_TLS_CERT), cert.pem()).expect("cert should write");
|
||||||
|
fs::write(dir.join(RUSTFS_TLS_KEY), signing_key.serialize_pem()).expect("key should write");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn execute_tls_inspect_accepts_valid_directory() {
|
||||||
|
let temp_dir = TempDir::new().expect("tempdir should create");
|
||||||
|
write_test_cert_pair(temp_dir.path());
|
||||||
|
|
||||||
|
let opts = TlsInspectOpts {
|
||||||
|
path: temp_dir.path().display().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
execute_tls_inspect(&opts).expect("tls inspect should succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_pair_summary_reports_missing_key() {
|
||||||
|
let pair = TlsCertPairInspection {
|
||||||
|
cert_path: "/tmp/rustfs_cert.pem".into(),
|
||||||
|
key_path: "/tmp/rustfs_key.pem".into(),
|
||||||
|
status: TlsCertPairStatus::MissingKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
let summary = format_pair_summary(&pair);
|
||||||
|
assert!(summary.contains("missing key"));
|
||||||
|
assert!(summary.contains("/tmp/rustfs_cert.pem"));
|
||||||
|
assert!(summary.contains("/tmp/rustfs_key.pem"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user