use crate::Result;
use crate::security::{
validate_cipher, validate_hostname, validate_port, validate_starttls_protocol,
};
use serde::{Deserialize, Serialize};
use std::process::{Command, Stdio};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct OpenSslClientOptions {
pub host: String,
pub port: u16,
pub starttls: Option<String>,
pub servername: Option<String>,
pub cipher: Option<String>,
pub tls_version: Option<String>,
pub showcerts: bool,
pub debug: bool,
pub state: bool,
pub timeout: Option<Duration>,
pub verify_locations: Option<String>,
pub cert: Option<String>,
pub key: Option<String>,
pub pass: Option<String>,
pub proxy: Option<String>,
pub alpn: Option<Vec<String>>,
pub bugs: bool,
pub reconnect: bool,
}
impl Default for OpenSslClientOptions {
fn default() -> Self {
Self {
host: String::new(),
port: 443,
starttls: None,
servername: None,
cipher: None,
tls_version: None,
showcerts: false,
debug: false,
state: false,
timeout: None,
verify_locations: None,
cert: None,
key: None,
pass: None,
proxy: None,
alpn: None,
bugs: false,
reconnect: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenSslClientResult {
pub success: bool,
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
pub connection_info: Option<ConnectionInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionInfo {
pub protocol: String,
pub cipher: String,
pub certificate_chain: Vec<String>,
pub verify_result: String,
pub session_details: String,
}
pub struct OpenSslClient {
openssl_path: String,
}
impl Default for OpenSslClient {
fn default() -> Self {
Self::new()
}
}
impl OpenSslClient {
pub fn new() -> Self {
Self {
openssl_path: "openssl".to_string(),
}
}
pub fn with_path(path: String) -> Self {
Self { openssl_path: path }
}
pub fn run(&self, options: &OpenSslClientOptions) -> Result<OpenSslClientResult> {
validate_hostname(&options.host)
.map_err(|e| crate::error::TlsError::Other(format!("Invalid hostname: {}", e)))?;
validate_port(options.port)
.map_err(|e| crate::error::TlsError::Other(format!("Invalid port: {}", e)))?;
let mut cmd = Command::new(&self.openssl_path);
cmd.arg("s_client");
cmd.arg("-connect");
cmd.arg(format!("{}:{}", options.host, options.port));
if let Some(ref protocol) = options.starttls {
validate_starttls_protocol(protocol).map_err(|e| {
crate::error::TlsError::Other(format!("Invalid STARTTLS protocol: {}", e))
})?;
cmd.arg("-starttls");
cmd.arg(protocol);
}
if let Some(ref servername) = options.servername {
validate_hostname(servername)
.map_err(|e| crate::error::TlsError::Other(format!("Invalid servername: {}", e)))?;
cmd.arg("-servername");
cmd.arg(servername);
}
if let Some(ref cipher) = options.cipher {
validate_cipher(cipher)
.map_err(|e| crate::error::TlsError::Other(format!("Invalid cipher: {}", e)))?;
cmd.arg("-cipher");
cmd.arg(cipher);
}
if let Some(ref version) = options.tls_version {
cmd.arg(version); }
if options.showcerts {
cmd.arg("-showcerts");
}
if options.debug {
cmd.arg("-debug");
}
if options.state {
cmd.arg("-state");
}
if let Some(timeout) = options.timeout {
cmd.arg("-timeout");
cmd.arg(timeout.as_secs().to_string());
}
if let Some(ref locations) = options.verify_locations {
cmd.arg("-CAfile");
cmd.arg(locations);
}
if let Some(ref cert) = options.cert {
cmd.arg("-cert");
cmd.arg(cert);
}
if let Some(ref key) = options.key {
cmd.arg("-key");
cmd.arg(key);
}
if let Some(ref pass) = options.pass {
cmd.arg("-pass");
cmd.arg(format!("pass:{}", pass));
}
if let Some(ref proxy) = options.proxy {
cmd.arg("-proxy");
cmd.arg(proxy);
}
if let Some(ref alpn) = options.alpn {
cmd.arg("-alpn");
cmd.arg(alpn.join(","));
}
if options.bugs {
cmd.arg("-bugs");
}
if options.reconnect {
cmd.arg("-reconnect");
}
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let output = cmd.output()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
let success = output.status.success();
let connection_info = if success {
Some(parse_connection_info(&stdout))
} else {
None
};
Ok(OpenSslClientResult {
success,
stdout,
stderr,
exit_code,
connection_info,
})
}
pub fn test_cipher(&self, host: &str, port: u16, cipher: &str) -> Result<bool> {
let options = OpenSslClientOptions {
host: host.to_string(),
port,
cipher: Some(cipher.to_string()),
servername: Some(host.to_string()),
..Default::default()
};
let result = self.run(&options)?;
Ok(result.success)
}
pub fn get_certificate_chain(&self, host: &str, port: u16) -> Result<Vec<String>> {
let options = OpenSslClientOptions {
host: host.to_string(),
port,
servername: Some(host.to_string()),
showcerts: true,
..Default::default()
};
let result = self.run(&options)?;
if result.success {
Ok(extract_certificates(&result.stdout))
} else {
Err(crate::error::TlsError::Other(
"Failed to get certificate chain".to_string(),
))
}
}
pub fn test_starttls(&self, host: &str, port: u16, protocol: &str) -> Result<bool> {
let options = OpenSslClientOptions {
host: host.to_string(),
port,
starttls: Some(protocol.to_string()),
servername: Some(host.to_string()),
..Default::default()
};
let result = self.run(&options)?;
Ok(result.success)
}
pub fn test_client_cert(&self, host: &str, port: u16, cert: &str, key: &str) -> Result<bool> {
let options = OpenSslClientOptions {
host: host.to_string(),
port,
servername: Some(host.to_string()),
cert: Some(cert.to_string()),
key: Some(key.to_string()),
..Default::default()
};
let result = self.run(&options)?;
Ok(result.success)
}
pub fn get_version(&self) -> Result<String> {
let output = Command::new(&self.openssl_path).arg("version").output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
Err(crate::error::TlsError::Other(
"Failed to get OpenSSL version".to_string(),
))
}
}
}
fn parse_connection_info(stdout: &str) -> ConnectionInfo {
let mut protocol = String::new();
let mut cipher = String::new();
let mut verify_result = String::new();
let mut session_details = String::new();
for line in stdout.lines() {
if line.contains("Protocol") && line.contains(":") {
protocol = line.split(':').nth(1).unwrap_or("").trim().to_string();
} else if line.contains("Cipher") && line.contains(":") {
cipher = line.split(':').nth(1).unwrap_or("").trim().to_string();
} else if line.contains("Verify return code:") {
verify_result = line.trim().to_string();
} else if line.starts_with("SSL-Session:") {
session_details = line.trim().to_string();
}
}
ConnectionInfo {
protocol,
cipher,
certificate_chain: extract_certificates(stdout),
verify_result,
session_details,
}
}
fn extract_certificates(stdout: &str) -> Vec<String> {
let mut certificates = Vec::new();
let mut current_cert = String::new();
let mut in_cert = false;
for line in stdout.lines() {
if line.contains("-----BEGIN CERTIFICATE-----") {
in_cert = true;
current_cert.clear();
current_cert.push_str(line);
current_cert.push('\n');
} else if line.contains("-----END CERTIFICATE-----") {
current_cert.push_str(line);
current_cert.push('\n');
certificates.push(current_cert.clone());
in_cert = false;
} else if in_cert {
current_cert.push_str(line);
current_cert.push('\n');
}
}
certificates
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_certificates() {
let stdout = r#"
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ
-----END CERTIFICATE-----
"#;
let certs = extract_certificates(stdout);
assert_eq!(certs.len(), 2);
}
#[test]
fn test_default_options() {
let options = OpenSslClientOptions::default();
assert_eq!(options.port, 443);
assert!(!options.showcerts);
assert!(!options.debug);
}
}