use std::time::Duration;
use tls_parser::nom::Err as NomErr;
use tls_parser::{
TlsExtension, TlsMessage, TlsMessageHandshake, parse_tls_client_hello_extensions,
parse_tls_plaintext,
};
use tokio::io::{AsyncRead, AsyncReadExt};
use tokio::time::timeout;
const PEEK_TIMEOUT: Duration = Duration::from_secs(5);
const PEEK_MAX: usize = 8192;
#[derive(Debug, PartialEq, Eq)]
enum Peek {
Found(String),
Absent,
Incomplete,
}
pub async fn tls_sni<R: AsyncRead + Unpin>(stream: &mut R, prefix: &mut Vec<u8>) -> Option<String> {
peek_with(stream, prefix, sni_from_client_hello).await
}
pub async fn http_host<R: AsyncRead + Unpin>(
stream: &mut R,
prefix: &mut Vec<u8>,
) -> Option<String> {
peek_with(stream, prefix, host_from_http).await
}
async fn peek_with<R, F>(stream: &mut R, prefix: &mut Vec<u8>, parse: F) -> Option<String>
where
R: AsyncRead + Unpin,
F: Fn(&[u8]) -> Peek,
{
let mut chunk = [0u8; 1024];
loop {
match parse(prefix) {
Peek::Found(host) => return Some(host),
Peek::Absent => return None,
Peek::Incomplete => {}
}
if prefix.len() >= PEEK_MAX {
return None;
}
match timeout(PEEK_TIMEOUT, stream.read(&mut chunk)).await {
Ok(Ok(n)) if n > 0 => prefix.extend_from_slice(&chunk[..n]),
_ => return None,
}
}
}
const TLS_HANDSHAKE_CONTENT_TYPE: u8 = 0x16;
fn sni_from_client_hello(buf: &[u8]) -> Peek {
if buf
.first()
.is_some_and(|&b| b != TLS_HANDSHAKE_CONTENT_TYPE)
{
return Peek::Absent;
}
let record = match parse_tls_plaintext(buf) {
Ok((_, record)) => record,
Err(NomErr::Incomplete(_)) => return Peek::Incomplete,
Err(_) => return Peek::Absent,
};
for msg in &record.msg {
let TlsMessage::Handshake(TlsMessageHandshake::ClientHello(hello)) = msg else {
continue;
};
let Some(ext) = hello.ext else {
return Peek::Absent;
};
return match parse_tls_client_hello_extensions(ext) {
Ok((_, extensions)) => extensions
.iter()
.find_map(sni_hostname)
.map_or(Peek::Absent, Peek::Found),
Err(NomErr::Incomplete(_)) => Peek::Incomplete,
Err(_) => Peek::Absent,
};
}
Peek::Absent
}
fn sni_hostname(ext: &TlsExtension) -> Option<String> {
let TlsExtension::SNI(names) = ext else {
return None;
};
names
.iter()
.find_map(|(_, raw)| std::str::from_utf8(raw).ok())
.map(str::to_ascii_lowercase)
}
fn host_from_http(buf: &[u8]) -> Peek {
let headers_end = find_subslice(buf, b"\r\n\r\n");
let region = match headers_end {
Some(end) => &buf[..end],
None => match buf.iter().rposition(|&b| b == b'\n') {
Some(pos) => &buf[..pos],
None => &[],
},
};
for line in region.split(|&b| b == b'\n') {
let line = line.strip_suffix(b"\r").unwrap_or(line);
if line.len() >= 5
&& line[..5].eq_ignore_ascii_case(b"host:")
&& let Some(host) = parse_host_value(&line[5..])
{
return Peek::Found(host);
}
}
if headers_end.is_some() {
Peek::Absent
} else {
Peek::Incomplete
}
}
fn parse_host_value(raw: &[u8]) -> Option<String> {
let value = std::str::from_utf8(raw).ok()?.trim();
if value.is_empty() {
return None;
}
let host = if let Some(rest) = value.strip_prefix('[') {
rest.split_once(']').map_or(value, |(addr, _)| addr)
} else if value.bytes().filter(|&b| b == b':').count() == 1 {
value.rsplit_once(':').map_or(value, |(host, _)| host)
} else {
value
};
Some(host.to_ascii_lowercase())
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[allow(clippy::cast_possible_truncation)]
fn client_hello_with_sni(sni: &str) -> Vec<u8> {
let sni = sni.as_bytes();
let mut sni_ext = Vec::new();
sni_ext.extend_from_slice(&((sni.len() + 3) as u16).to_be_bytes()); sni_ext.push(0); sni_ext.extend_from_slice(&(sni.len() as u16).to_be_bytes());
sni_ext.extend_from_slice(sni);
let mut extensions = Vec::new();
extensions.extend_from_slice(&0u16.to_be_bytes());
extensions.extend_from_slice(&(sni_ext.len() as u16).to_be_bytes());
extensions.extend_from_slice(&sni_ext);
let mut body = Vec::new();
body.extend_from_slice(&[0x03, 0x03]); body.extend_from_slice(&[0u8; 32]); body.push(0); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&[0x00, 0x2f]); body.push(1); body.push(0); body.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
body.extend_from_slice(&extensions);
let mut handshake = Vec::new();
handshake.push(0x01);
let blen = body.len();
handshake.extend_from_slice(&[(blen >> 16) as u8, (blen >> 8) as u8, blen as u8]);
handshake.extend_from_slice(&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 sni_parser_extracts_hostname() {
let hello = client_hello_with_sni("Example.COM");
assert_eq!(
sni_from_client_hello(&hello),
Peek::Found("example.com".to_string())
);
}
#[test]
fn sni_parser_reports_incomplete_on_partial_record() {
let hello = client_hello_with_sni("example.com");
assert_eq!(sni_from_client_hello(&hello[..10]), Peek::Incomplete);
}
#[test]
fn sni_parser_rejects_non_tls() {
assert_eq!(
sni_from_client_hello(b"GET / HTTP/1.1\r\n\r\n"),
Peek::Absent
);
}
#[tokio::test]
async fn tls_sni_reads_from_stream_and_captures_prefix() {
let hello = client_hello_with_sni("api.example.com");
let mut stream = Cursor::new(hello.clone());
let mut prefix = Vec::new();
let sni = tls_sni(&mut stream, &mut prefix).await;
assert_eq!(sni.as_deref(), Some("api.example.com"));
assert_eq!(prefix, hello);
}
#[test]
fn http_host_parser_extracts_host() {
let req = b"GET /path HTTP/1.1\r\nHost: Example.com\r\nAccept: */*\r\n\r\n";
assert_eq!(host_from_http(req), Peek::Found("example.com".to_string()));
}
#[test]
fn http_host_parser_strips_port() {
let req = b"GET / HTTP/1.1\r\nHost: example.com:8443\r\n\r\n";
assert_eq!(host_from_http(req), Peek::Found("example.com".to_string()));
}
#[test]
fn http_host_parser_incomplete_without_terminator() {
let req = b"GET / HTTP/1.1\r\nHos";
assert_eq!(host_from_http(req), Peek::Incomplete);
}
#[test]
fn http_host_parser_absent_when_headers_end_without_host() {
let req = b"GET / HTTP/1.1\r\nAccept: */*\r\n\r\n";
assert_eq!(host_from_http(req), Peek::Absent);
}
#[tokio::test]
async fn http_host_reads_from_stream() {
let req = b"GET / HTTP/1.1\r\nHost: svc.internal\r\n\r\n".to_vec();
let mut stream = Cursor::new(req.clone());
let mut prefix = Vec::new();
let host = http_host(&mut stream, &mut prefix).await;
assert_eq!(host.as_deref(), Some("svc.internal"));
assert_eq!(prefix, req);
}
}