feat: Implement SSH master connector and CLI for certificate management
This commit is contained in:
132
apps/nxmesh-agent/src/connector/master/ssh.rs
Normal file
132
apps/nxmesh-agent/src/connector/master/ssh.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::{fs::File, io::Read, sync::Arc};
|
||||
|
||||
use tokio::{fs::read, sync::Mutex};
|
||||
|
||||
use nxmesh_proto::agent_service_client::AgentServiceClient;
|
||||
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::config::settings::{self, MAuthSettings, TLSSettings};
|
||||
|
||||
use super::{AgentClient, MasterConnectorTrait};
|
||||
|
||||
pub struct SshMasterConnector {
|
||||
client: Arc<Mutex<AgentClient>>,
|
||||
}
|
||||
|
||||
impl SshMasterConnector {
|
||||
pub async fn new(
|
||||
settings: crate::config::settings::GrpcSettings,
|
||||
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let tls_config = Self::generate_tls_config(&settings.m_auth).await?;
|
||||
// Create a gRPC channel
|
||||
let endpoint = tonic::transport::Channel::from_shared(settings.connection_string.clone())
|
||||
.map_err(|e| format!("Failed to create gRPC endpoint: {}", e))?
|
||||
.tls_config(tls_config)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to set TLS config: {}. Ensure TLS settings and certificates are correct.",
|
||||
e
|
||||
)
|
||||
})?
|
||||
.connect_timeout(std::time::Duration::from_secs(5))
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.connect_lazy();
|
||||
|
||||
// Create the gRPC client
|
||||
let client = Arc::new(Mutex::new(AgentServiceClient::new(endpoint)));
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
async fn generate_tls_config(
|
||||
settings: &MAuthSettings,
|
||||
) -> Result<ClientTlsConfig, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let tls_config = match &settings {
|
||||
MAuthSettings::Tls(tls_settings) => {
|
||||
let (ca, cert, key) = match tls_settings {
|
||||
TLSSettings::RawPath {
|
||||
ca_path,
|
||||
cert_path,
|
||||
key_path,
|
||||
} => {
|
||||
// Read the certificate and key from the specified file paths
|
||||
let ca = read(ca_path).await?;
|
||||
let cert = read(cert_path).await?;
|
||||
let key = read(key_path).await?;
|
||||
(ca, cert, key)
|
||||
}
|
||||
TLSSettings::ZipPath { cert_zip_path } => {
|
||||
// Extract the certificate and key from the zip file
|
||||
Self::extract_certificate(cert_zip_path).await?
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: allow skipping SANs validation if specified in the settings
|
||||
ClientTlsConfig::new()
|
||||
.ca_certificate(Certificate::from_pem(&ca))
|
||||
.identity(Identity::from_pem(&cert, &key))
|
||||
}
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => {
|
||||
return Err("TLS settings are required for SSH connection".into());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(tls_config)
|
||||
}
|
||||
|
||||
async fn extract_certificate(
|
||||
cert_zip_path: &str,
|
||||
) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// unzip the file and extract the cert, ca and key
|
||||
let file = File::open(cert_zip_path)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
let mut cert = Vec::new();
|
||||
let mut key = Vec::new();
|
||||
let mut ca = Vec::new();
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let outpath = match file.enclosed_name() {
|
||||
Some(path) => path.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
let file_name = outpath
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or_default();
|
||||
if file_name != "cert.pem" && file_name != "key.pem" && file_name != "ca.pem" {
|
||||
warn!("Unexpected file in certificate zip: {}", file_name);
|
||||
continue;
|
||||
}
|
||||
if file_name == "cert.pem" {
|
||||
file.read_to_end(&mut cert)?;
|
||||
} else if file_name == "key.pem" {
|
||||
file.read_to_end(&mut key)?;
|
||||
} else if file_name == "ca.pem" {
|
||||
file.read_to_end(&mut ca)?;
|
||||
}
|
||||
}
|
||||
|
||||
if cert.is_empty() || key.is_empty() || ca.is_empty() {
|
||||
return Err("Certificate zip must contain cert.pem, key.pem and ca.pem".into());
|
||||
}
|
||||
|
||||
Ok((ca, cert, key))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MasterConnectorTrait for SshMasterConnector {
|
||||
async fn connect(
|
||||
&mut self,
|
||||
_settings: &crate::config::settings::Settings,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// ensure connection if required
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_client(&self) -> Arc<Mutex<AgentClient>> {
|
||||
self.client.clone()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user