use crate::SessionParser;
use super::parser::{self, DirState, ParseOutput};
use super::types::{TlsAlert, TlsClientHello, TlsConfig, TlsServerHello};
#[derive(Debug, Clone)]
pub enum TlsMessage {
ClientHello(Box<TlsClientHello>),
ServerHello(Box<TlsServerHello>),
Alert(TlsAlert),
#[cfg(feature = "ja3")]
Ja3 {
hash: String,
canonical: String,
},
}
#[derive(Debug, Clone)]
pub struct TlsParser {
config: TlsConfig,
init_buf: Vec<u8>,
init_state: DirState,
resp_buf: Vec<u8>,
resp_state: DirState,
}
impl Default for TlsParser {
fn default() -> Self {
Self::with_config(TlsConfig::default())
}
}
impl TlsParser {
pub fn with_config(config: TlsConfig) -> Self {
Self {
config,
init_buf: Vec::with_capacity(4096),
init_state: DirState::Reading,
resp_buf: Vec::with_capacity(4096),
resp_state: DirState::Reading,
}
}
fn drain(
state: &mut DirState,
buf: &mut Vec<u8>,
is_initiator: bool,
cfg: &TlsConfig,
out: &mut Vec<TlsMessage>,
) {
loop {
match parser::step(state, buf, is_initiator, cfg) {
Ok(Some(parsed)) => Self::dispatch(parsed, cfg, out),
Ok(None) => break,
Err(_) => {
buf.clear();
break;
}
}
}
}
fn dispatch(parsed: ParseOutput, cfg: &TlsConfig, out: &mut Vec<TlsMessage>) {
match parsed {
ParseOutput::ClientHello(ch) => {
#[cfg(feature = "ja3")]
if cfg.ja3 {
let (canonical, hash) = super::fingerprint::ja3(&ch);
out.push(TlsMessage::Ja3 { hash, canonical });
}
#[cfg(not(feature = "ja3"))]
{
let _ = cfg; }
out.push(TlsMessage::ClientHello(ch));
}
ParseOutput::ServerHello(sh) => out.push(TlsMessage::ServerHello(sh)),
ParseOutput::Alert(a) => out.push(TlsMessage::Alert(a)),
}
}
}
impl SessionParser for TlsParser {
type Message = TlsMessage;
fn feed_initiator(&mut self, bytes: &[u8]) -> Vec<TlsMessage> {
if bytes.is_empty() || matches!(self.init_state, DirState::Encrypted | DirState::Desynced) {
return Vec::new();
}
self.init_buf.extend_from_slice(bytes);
let mut out = Vec::new();
Self::drain(
&mut self.init_state,
&mut self.init_buf,
true,
&self.config,
&mut out,
);
out
}
fn feed_responder(&mut self, bytes: &[u8]) -> Vec<TlsMessage> {
if bytes.is_empty() || matches!(self.resp_state, DirState::Encrypted | DirState::Desynced) {
return Vec::new();
}
self.resp_buf.extend_from_slice(bytes);
let mut out = Vec::new();
Self::drain(
&mut self.resp_state,
&mut self.resp_buf,
false,
&self.config,
&mut out,
);
out
}
fn rst_initiator(&mut self) {
self.init_buf.clear();
self.init_state = DirState::Reading;
}
fn rst_responder(&mut self) {
self.resp_buf.clear();
self.resp_state = DirState::Reading;
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
fn build_client_hello_record() -> Vec<u8> {
let mut ch_body = Vec::new();
ch_body.extend_from_slice(&[0x03, 0x03]); ch_body.extend_from_slice(&[0u8; 32]); ch_body.push(0); ch_body.extend_from_slice(&[0, 2, 0x13, 0x01]);
ch_body.extend_from_slice(&[1, 0]);
ch_body.extend_from_slice(&[0, 0]);
let mut handshake = Vec::new();
handshake.push(0x01);
let len = ch_body.len();
handshake.push(((len >> 16) & 0xff) as u8);
handshake.push(((len >> 8) & 0xff) as u8);
handshake.push((len & 0xff) as u8);
handshake.extend_from_slice(&ch_body);
let mut record = Vec::new();
record.push(0x16);
record.extend_from_slice(&[0x03, 0x01]);
record.extend_from_slice(&(handshake.len() as u16).to_be_bytes());
record.extend_from_slice(&handshake);
record
}
#[test]
fn parses_client_hello() {
let mut p = TlsParser::default();
let bytes = build_client_hello_record();
let messages = p.feed_initiator(&bytes);
assert!(
messages
.iter()
.any(|m| matches!(m, TlsMessage::ClientHello(_))),
"expected at least one ClientHello, got {messages:?}"
);
}
#[test]
fn split_segments_concatenate() {
let mut p = TlsParser::default();
let bytes = build_client_hello_record();
let mut all_msgs = Vec::new();
for chunk in bytes.chunks(1) {
all_msgs.extend(p.feed_initiator(chunk));
}
assert!(
all_msgs
.iter()
.any(|m| matches!(m, TlsMessage::ClientHello(_))),
"expected ClientHello after byte-at-a-time feed, got {all_msgs:?}"
);
}
#[test]
fn rst_clears_state() {
let mut p = TlsParser::default();
p.feed_initiator(&[0x16, 0x03, 0x01, 0x00]); p.rst_initiator();
assert!(p.init_buf.is_empty());
assert_eq!(p.init_state, DirState::Reading);
}
#[test]
fn empty_feed_returns_empty() {
let mut p = TlsParser::default();
assert!(p.feed_initiator(&[]).is_empty());
assert!(p.feed_responder(&[]).is_empty());
}
#[allow(dead_code)]
fn _bytes_used() -> Bytes {
Bytes::new()
}
}