use crate::error::{Error, Result};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ClientHelloInfo {
pub server_name: Option<String>,
pub alpn_protocols: Vec<Vec<u8>>,
}
impl ClientHelloInfo {
pub fn wants_acme_tls(&self) -> bool {
self.alpn_protocols.iter().any(|p| p == b"acme-tls/1")
}
}
pub fn peek(buf: &[u8]) -> Result<Option<ClientHelloInfo>> {
match purecrypto::tls::peek_client_hello(buf) {
Ok(Some(info)) => Ok(Some(ClientHelloInfo {
server_name: info.server_name,
alpn_protocols: info.alpn_protocols,
})),
Ok(None) => Ok(None),
Err(e) => Err(Error::Tls(format!("client hello: {e:?}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn client_hello(sni: Option<&str>, alpn: &[&[u8]]) -> Vec<u8> {
let mut ext = Vec::new();
if let Some(host) = sni {
let mut sn = Vec::new();
sn.extend_from_slice(&((host.len() + 3) as u16).to_be_bytes()); sn.push(0); sn.extend_from_slice(&(host.len() as u16).to_be_bytes());
sn.extend_from_slice(host.as_bytes());
ext.extend_from_slice(&0x0000u16.to_be_bytes());
ext.extend_from_slice(&(sn.len() as u16).to_be_bytes());
ext.extend_from_slice(&sn);
}
if !alpn.is_empty() {
let mut list = Vec::new();
for p in alpn {
list.push(p.len() as u8);
list.extend_from_slice(p);
}
let mut a = Vec::new();
a.extend_from_slice(&(list.len() as u16).to_be_bytes());
a.extend_from_slice(&list);
ext.extend_from_slice(&0x0010u16.to_be_bytes());
ext.extend_from_slice(&(a.len() as u16).to_be_bytes());
ext.extend_from_slice(&a);
}
let mut body = Vec::new();
body.extend_from_slice(&0x0303u16.to_be_bytes()); body.extend_from_slice(&[0u8; 32]); body.push(0); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&0x1301u16.to_be_bytes()); body.push(1); body.push(0); body.extend_from_slice(&(ext.len() as u16).to_be_bytes());
body.extend_from_slice(&ext);
let mut hs = Vec::new();
hs.push(1); let bl = body.len();
hs.extend_from_slice(&[(bl >> 16) as u8, (bl >> 8) as u8, bl as u8]);
hs.extend_from_slice(&body);
let mut rec = Vec::new();
rec.push(22); rec.extend_from_slice(&0x0301u16.to_be_bytes());
rec.extend_from_slice(&(hs.len() as u16).to_be_bytes());
rec.extend_from_slice(&hs);
rec
}
#[test]
fn extracts_sni_and_alpn() {
let rec = client_hello(Some("example.test"), &[b"h2", b"http/1.1"]);
let info = peek(&rec).unwrap().unwrap();
assert_eq!(info.server_name.as_deref(), Some("example.test"));
assert_eq!(
info.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
);
assert!(!info.wants_acme_tls());
}
#[test]
fn detects_acme_tls() {
let rec = client_hello(Some("a.test"), &[b"acme-tls/1"]);
assert!(peek(&rec).unwrap().unwrap().wants_acme_tls());
}
#[test]
fn no_sni_is_ok() {
let rec = client_hello(None, &[]);
let info = peek(&rec).unwrap().unwrap();
assert!(info.server_name.is_none());
assert!(info.alpn_protocols.is_empty());
}
#[test]
fn partial_returns_none() {
let rec = client_hello(Some("example.test"), &[b"h2"]);
assert!(peek(&rec[..8]).unwrap().is_none());
assert!(peek(&rec[..rec.len() - 5]).unwrap().is_none());
}
#[test]
fn non_tls_never_yields_client_hello() {
assert!(!matches!(peek(b"GET / HTTP/1.1\r\n\r\n"), Ok(Some(_))));
}
}