use crate::utils::network::Target;
use crate::{Result, tls_bail};
use openssl::ssl::{SslConnector, SslMethod, SslSessionCacheMode};
use serde::{Deserialize, Serialize};
use tokio::net::TcpStream;
use tokio::time::{Duration, timeout};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionResumptionResult {
pub session_id_reuse: SessionIdTest,
pub session_ticket: SessionTicketTest,
pub resumption_support: ResumptionSupport,
pub performance_gain: Option<f64>, pub details: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionIdTest {
pub supported: bool,
pub session_id_length: Option<usize>,
pub reuse_successful: bool,
pub connections_tested: usize,
pub reuse_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionTicketTest {
pub supported: bool,
pub ticket_lifetime: Option<u32>, pub ticket_size: Option<usize>,
pub reuse_successful: bool,
pub new_ticket_on_resume: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResumptionSupport {
Full, SessionIdOnly, TicketOnly, None, }
impl ResumptionSupport {
pub fn as_str(&self) -> &'static str {
match self {
ResumptionSupport::Full => "Full (Session ID + Tickets)",
ResumptionSupport::SessionIdOnly => "Session ID Only",
ResumptionSupport::TicketOnly => "Session Tickets Only",
ResumptionSupport::None => "None",
}
}
}
pub struct SessionResumptionTester {
target: Target,
}
impl SessionResumptionTester {
pub fn new(target: Target) -> Self {
Self { target }
}
pub async fn test(&self) -> Result<SessionResumptionResult> {
let session_id = self.test_session_id_reuse().await?;
let session_ticket = self.test_session_ticket().await?;
let resumption_support = if session_id.supported && session_ticket.supported {
ResumptionSupport::Full
} else if session_id.supported {
ResumptionSupport::SessionIdOnly
} else if session_ticket.supported {
ResumptionSupport::TicketOnly
} else {
ResumptionSupport::None
};
let performance_gain = if session_id.reuse_successful || session_ticket.reuse_successful {
Some(self.measure_performance_gain().await.unwrap_or(0.0))
} else {
None
};
let details = format!(
"Session resumption: {}. Session ID reuse: {}/{}. Ticket reuse: {}",
resumption_support.as_str(),
session_id.reuse_count,
session_id.connections_tested,
session_ticket.reuse_successful
);
Ok(SessionResumptionResult {
session_id_reuse: session_id,
session_ticket,
resumption_support,
performance_gain,
details,
})
}
async fn test_session_id_reuse(&self) -> Result<SessionIdTest> {
let addr = self.target.socket_addrs()[0];
let connect_timeout = Duration::from_secs(10);
let stream = timeout(connect_timeout, TcpStream::connect(&addr))
.await
.map_err(|_| anyhow::anyhow!("Connection timeout"))??;
let std_stream = stream.into_std()?;
let mut builder = SslConnector::builder(SslMethod::tls())?;
builder.set_session_cache_mode(SslSessionCacheMode::CLIENT);
let connector = builder.build();
let ssl_stream = connector.connect(&self.target.hostname, std_stream)?;
let (session_id_length, has_session_id) = {
if let Some(session) = ssl_stream.ssl().session() {
let len = session.id().len();
(len, len > 0)
} else {
tls_bail!("No session established");
}
};
drop(ssl_stream);
if !has_session_id {
return Ok(SessionIdTest {
supported: false,
session_id_length: None,
reuse_successful: false,
connections_tested: 1,
reuse_count: 0,
});
}
let mut reuse_count = 0;
let connections_tested = 5;
for _ in 0..connections_tested {
if let Ok(resumed) = self.try_resume_with_session().await
&& resumed
{
reuse_count += 1;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let reuse_successful = reuse_count > 0;
Ok(SessionIdTest {
supported: true,
session_id_length: Some(session_id_length),
reuse_successful,
connections_tested,
reuse_count,
})
}
async fn try_resume_with_session(&self) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
let connect_timeout = Duration::from_secs(10);
let stream = timeout(connect_timeout, TcpStream::connect(&addr))
.await
.map_err(|_| anyhow::anyhow!("Connection timeout"))??;
let std_stream = stream.into_std()?;
let mut builder = SslConnector::builder(SslMethod::tls())?;
builder.set_session_cache_mode(SslSessionCacheMode::CLIENT);
let connector = builder.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(ssl_stream) => {
let reused = ssl_stream.ssl().session_reused();
Ok(reused)
}
Err(_) => Ok(false),
}
}
async fn test_session_ticket(&self) -> Result<SessionTicketTest> {
let addr = self.target.socket_addrs()[0];
let connect_timeout = Duration::from_secs(10);
let stream = timeout(connect_timeout, TcpStream::connect(&addr))
.await
.map_err(|_| anyhow::anyhow!("Connection timeout"))??;
let std_stream = stream.into_std()?;
let mut builder = SslConnector::builder(SslMethod::tls())?;
builder.set_session_cache_mode(SslSessionCacheMode::CLIENT);
let connector = builder.build();
let ssl_stream = connector.connect(&self.target.hostname, std_stream)?;
let has_ticket = {
if let Some(session) = ssl_stream.ssl().session() {
session.id().is_empty()
} else {
false
}
};
drop(ssl_stream);
if !has_ticket {
return Ok(SessionTicketTest {
supported: false,
ticket_lifetime: None,
ticket_size: None,
reuse_successful: false,
new_ticket_on_resume: false,
});
}
let reuse_successful = self.try_resume_with_ticket().await.unwrap_or(false);
Ok(SessionTicketTest {
supported: true,
ticket_lifetime: None, ticket_size: None, reuse_successful,
new_ticket_on_resume: false, })
}
async fn try_resume_with_ticket(&self) -> Result<bool> {
let addr = self.target.socket_addrs()[0];
let connect_timeout = Duration::from_secs(10);
let stream = timeout(connect_timeout, TcpStream::connect(&addr))
.await
.map_err(|_| anyhow::anyhow!("Connection timeout"))??;
let std_stream = stream.into_std()?;
let mut builder = SslConnector::builder(SslMethod::tls())?;
builder.set_session_cache_mode(SslSessionCacheMode::CLIENT);
let connector = builder.build();
match connector.connect(&self.target.hostname, std_stream) {
Ok(ssl_stream) => {
let reused = ssl_stream.ssl().session_reused();
Ok(reused)
}
Err(_) => Ok(false),
}
}
async fn measure_performance_gain(&self) -> Result<f64> {
use std::time::Instant;
let full_handshake_times: Vec<f64> = (0..5)
.filter_map(|_| {
let start = Instant::now();
if self.perform_full_handshake_sync().is_ok() {
Some(start.elapsed().as_secs_f64())
} else {
None
}
})
.collect();
if full_handshake_times.is_empty() {
return Ok(0.0);
}
let avg_full = full_handshake_times.iter().sum::<f64>() / full_handshake_times.len() as f64;
let resumed_handshake_times: Vec<f64> = (0..5)
.filter_map(|_| {
let start = Instant::now();
if self.perform_resumed_handshake_sync().is_ok() {
Some(start.elapsed().as_secs_f64())
} else {
None
}
})
.collect();
if resumed_handshake_times.is_empty() {
return Ok(0.0);
}
let avg_resumed =
resumed_handshake_times.iter().sum::<f64>() / resumed_handshake_times.len() as f64;
let improvement = if avg_full > 0.0 {
((avg_full - avg_resumed) / avg_full) * 100.0
} else {
0.0
};
Ok(improvement)
}
fn perform_full_handshake_sync(&self) -> Result<()> {
use std::net::TcpStream as StdTcpStream;
let addr = self.target.socket_addrs()[0];
let stream = StdTcpStream::connect(addr)?;
let builder = SslConnector::builder(SslMethod::tls())?;
let connector = builder.build();
let _ssl_stream = connector.connect(&self.target.hostname, stream)?;
Ok(())
}
fn perform_resumed_handshake_sync(&self) -> Result<()> {
use std::net::TcpStream as StdTcpStream;
let addr = self.target.socket_addrs()[0];
let stream1 = StdTcpStream::connect(addr)?;
let mut builder1 = SslConnector::builder(SslMethod::tls())?;
builder1.set_session_cache_mode(SslSessionCacheMode::CLIENT);
let connector1 = builder1.build();
let ssl_stream1 = connector1.connect(&self.target.hostname, stream1)?;
let _session = ssl_stream1
.ssl()
.session()
.ok_or_else(|| anyhow::anyhow!("No session"))?;
drop(ssl_stream1);
let stream2 = StdTcpStream::connect(addr)?;
let mut builder2 = SslConnector::builder(SslMethod::tls())?;
builder2.set_session_cache_mode(SslSessionCacheMode::CLIENT);
let connector2 = builder2.build();
let _ssl_stream2 = connector2.connect(&self.target.hostname, stream2)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resumption_support_display() {
assert_eq!(
ResumptionSupport::Full.as_str(),
"Full (Session ID + Tickets)"
);
assert_eq!(ResumptionSupport::SessionIdOnly.as_str(), "Session ID Only");
assert_eq!(
ResumptionSupport::TicketOnly.as_str(),
"Session Tickets Only"
);
assert_eq!(ResumptionSupport::None.as_str(), "None");
}
}