#![allow(dead_code)]
use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tirith_core::capsule::NetworkPolicy;
use tirith_core::url_validate::{is_cloud_metadata_addr, is_public_addr};
#[derive(Debug, Clone)]
pub struct BrokerLimits {
pub max_connections: u64,
pub max_bytes_per_direction: u64,
pub max_resolved_addrs: usize,
pub handshake_timeout: Duration,
pub idle_timeout: Duration,
}
impl Default for BrokerLimits {
fn default() -> Self {
BrokerLimits {
max_connections: 64,
max_bytes_per_direction: 256 * 1024 * 1024,
max_resolved_addrs: 8,
handshake_timeout: Duration::from_secs(10),
idle_timeout: Duration::from_secs(120),
}
}
}
#[derive(Debug, Clone)]
pub struct BrokerConfig {
pub network: NetworkPolicy,
pub session_token: String,
pub allow_plaintext: bool,
pub limits: BrokerLimits,
}
impl BrokerConfig {
pub fn new(network: NetworkPolicy, session_token: String) -> Result<Self, String> {
if session_token.is_empty() {
return Err(
"broker session token must not be empty (an empty token would authenticate \
an anonymous client)"
.to_string(),
);
}
Ok(BrokerConfig {
network,
session_token,
allow_plaintext: false,
limits: BrokerLimits::default(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConnectDecision {
Allow {
host: String,
port: u16,
approved_ip: IpAddr,
},
Deny { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConnectRequest {
pub host: String,
pub port: u16,
pub bearer_token: Option<String>,
}
pub fn parse_connect_head(head: &str) -> Result<ConnectRequest, String> {
let mut lines = head.split("\r\n");
let request_line = lines.next().ok_or_else(|| "empty request".to_string())?;
let mut parts = request_line.split(' ');
let method = parts.next().unwrap_or("");
if !method.eq_ignore_ascii_case("CONNECT") {
return Err(format!("method not allowed: {method}"));
}
let authority = parts
.next()
.ok_or_else(|| "missing CONNECT authority".to_string())?;
let (host, port) = split_authority(authority)?;
let mut bearer_token = None;
for line in lines {
if line.is_empty() {
break;
}
if let Some((name, value)) = line.split_once(':') {
if name.trim().eq_ignore_ascii_case("proxy-authorization") {
let value = value.trim();
if let Some(rest) = value.strip_prefix("Bearer ") {
bearer_token = Some(rest.trim().to_string());
} else if let Some(rest) = value.strip_prefix("bearer ") {
bearer_token = Some(rest.trim().to_string());
}
}
}
}
Ok(ConnectRequest {
host,
port,
bearer_token,
})
}
fn split_authority(authority: &str) -> Result<(String, u16), String> {
if let Some(rest) = authority.strip_prefix('[') {
let (addr, port_part) = rest
.split_once(']')
.ok_or_else(|| "malformed IPv6 authority".to_string())?;
let port = port_part
.strip_prefix(':')
.ok_or_else(|| "IPv6 authority missing port".to_string())?
.parse::<u16>()
.map_err(|_| "invalid port".to_string())?;
if addr.is_empty() {
return Err("empty host".to_string());
}
return Ok((addr.to_string(), port));
}
let (host, port_str) = authority
.rsplit_once(':')
.ok_or_else(|| "authority missing port".to_string())?;
if host.is_empty() {
return Err("empty host".to_string());
}
let port = port_str
.parse::<u16>()
.map_err(|_| "invalid port".to_string())?;
Ok((host.to_string(), port))
}
pub fn decide_connect(
cfg: &BrokerConfig,
req: &ConnectRequest,
resolved: &[IpAddr],
) -> ConnectDecision {
let presented = req.bearer_token.as_deref().unwrap_or("");
if presented.is_empty() || cfg.session_token.is_empty() {
return ConnectDecision::Deny {
reason: "missing or invalid proxy-authorization token".to_string(),
};
}
if !constant_time_eq(presented.as_bytes(), cfg.session_token.as_bytes()) {
return ConnectDecision::Deny {
reason: "missing or invalid proxy-authorization token".to_string(),
};
}
if !cfg.network.permits(&req.host, req.port) {
return ConnectDecision::Deny {
reason: format!("policy denies {}:{}", req.host, req.port),
};
}
if resolved.len() > cfg.limits.max_resolved_addrs {
return ConnectDecision::Deny {
reason: format!(
"host resolved to {} addresses (cap {})",
resolved.len(),
cfg.limits.max_resolved_addrs
),
};
}
let approved = resolved.iter().copied().find(|ip| {
let sock = SocketAddr::new(*ip, req.port);
is_public_addr(&sock) && !is_cloud_metadata_addr(&sock)
});
match approved {
Some(ip) => ConnectDecision::Allow {
host: req.host.clone(),
port: req.port,
approved_ip: ip,
},
None => ConnectDecision::Deny {
reason: format!("{} resolves to no public address", req.host),
},
}
}
pub fn extract_sni(buf: &[u8]) -> Option<String> {
if buf.len() < 5 {
return None;
}
if buf[0] != 22 {
return None;
}
let record_len = u16::from_be_bytes([buf[3], buf[4]]) as usize;
let record = buf.get(5..5 + record_len)?;
if record.len() < 4 {
return None;
}
if record[0] != 1 {
return None;
}
let hs_len = ((record[1] as usize) << 16) | ((record[2] as usize) << 8) | (record[3] as usize);
let body = record.get(4..4 + hs_len)?;
let mut p = 0usize;
p = p.checked_add(2)?; p = p.checked_add(32)?; let sid_len = *body.get(p)? as usize;
p = p.checked_add(1)?.checked_add(sid_len)?;
let cs_len = u16::from_be_bytes([*body.get(p)?, *body.get(p + 1)?]) as usize;
p = p.checked_add(2)?.checked_add(cs_len)?;
let cm_len = *body.get(p)? as usize;
p = p.checked_add(1)?.checked_add(cm_len)?;
let ext_total = u16::from_be_bytes([*body.get(p)?, *body.get(p + 1)?]) as usize;
p = p.checked_add(2)?;
let ext_end = p.checked_add(ext_total)?;
if ext_end > body.len() {
return None;
}
while p + 4 <= ext_end {
let ext_type = u16::from_be_bytes([body[p], body[p + 1]]);
let ext_len = u16::from_be_bytes([body[p + 2], body[p + 3]]) as usize;
p += 4;
let ext_data = body.get(p..p.checked_add(ext_len)?)?;
if ext_type == 0 {
return parse_server_name_list(ext_data);
}
p += ext_len;
}
None
}
fn parse_server_name_list(data: &[u8]) -> Option<String> {
if data.len() < 2 {
return None;
}
let list_len = u16::from_be_bytes([data[0], data[1]]) as usize;
let list = data.get(2..2 + list_len)?;
let mut q = 0usize;
while q + 3 <= list.len() {
let name_type = list[q];
let name_len = u16::from_be_bytes([list[q + 1], list[q + 2]]) as usize;
q += 3;
let name = list.get(q..q.checked_add(name_len)?)?;
if name_type == 0 {
let host = std::str::from_utf8(name).ok()?;
return Some(host.trim_end_matches('.').to_ascii_lowercase());
}
q += name_len;
}
None
}
pub fn sni_matches_host(sni: Option<&str>, host: &str) -> bool {
let host_norm = host.trim_end_matches('.').to_ascii_lowercase();
if host_norm.parse::<IpAddr>().is_ok() {
return true;
}
match sni {
Some(name) => name.trim_end_matches('.').to_ascii_lowercase() == host_norm,
None => false,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokerAuditEvent {
pub host: String,
pub port: u16,
pub approved_ip: Option<IpAddr>,
pub allowed: bool,
pub reason: String,
pub bytes_up: u64,
pub bytes_down: u64,
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let mut diff: u8 = ((a.len() ^ b.len()) != 0) as u8;
let n = a.len().max(b.len());
for i in 0..n {
let x = a.get(i).copied().unwrap_or(0);
let y = b.get(i).copied().unwrap_or(0);
diff |= x ^ y;
}
diff == 0
}
#[derive(Debug, Default)]
pub struct BrokerState {
active: AtomicU64,
}
impl BrokerState {
fn try_acquire(&self, max: u64) -> bool {
let prev = self.active.fetch_add(1, Ordering::SeqCst);
if prev >= max {
self.active.fetch_sub(1, Ordering::SeqCst);
false
} else {
true
}
}
fn release(&self) {
self.active.fetch_sub(1, Ordering::SeqCst);
}
pub fn active(&self) -> u64 {
self.active.load(Ordering::SeqCst)
}
}
pub async fn run_broker<F>(listener: TcpListener, cfg: Arc<BrokerConfig>, audit: Arc<F>)
where
F: Fn(BrokerAuditEvent) + Send + Sync + 'static,
{
let state = Arc::new(BrokerState::default());
loop {
let (stream, peer) = match listener.accept().await {
Ok(pair) => pair,
Err(_) => continue,
};
if !peer.ip().is_loopback() {
drop(stream);
continue;
}
let cfg = Arc::clone(&cfg);
let audit = Arc::clone(&audit);
let state = Arc::clone(&state);
if !state.try_acquire(cfg.limits.max_connections) {
tokio::spawn(async move {
let mut s = stream;
let _ = s
.write_all(b"HTTP/1.1 503 Service Unavailable\r\n\r\n")
.await;
let _ = s.shutdown().await;
});
continue;
}
tokio::spawn(async move {
let _guard = ConnGuard {
state: Arc::clone(&state),
};
if let Err(reason) = serve_connection(stream, &cfg, &audit).await {
let _ = reason;
}
});
}
}
struct ConnGuard {
state: Arc<BrokerState>,
}
impl Drop for ConnGuard {
fn drop(&mut self) {
self.state.release();
}
}
async fn serve_connection<F>(
mut client: TcpStream,
cfg: &BrokerConfig,
audit: &Arc<F>,
) -> Result<(), String>
where
F: Fn(BrokerAuditEvent) + Send + Sync + 'static,
{
let head =
match tokio::time::timeout(cfg.limits.handshake_timeout, read_request_head(&mut client))
.await
{
Ok(Ok(h)) => h,
Ok(Err(e)) => {
deny(&mut client, audit, "", 0, None, &e).await;
return Err(e);
}
Err(_) => {
deny(&mut client, audit, "", 0, None, "request head timeout").await;
return Err("request head timeout".to_string());
}
};
let req = match parse_connect_head(&head) {
Ok(r) => r,
Err(e) => {
deny(&mut client, audit, "", 0, None, &e).await;
return Err(e);
}
};
let host_for_lookup = req.host.clone();
let port = req.port;
let resolved: Vec<IpAddr> = match tokio::task::spawn_blocking(move || {
use std::net::ToSocketAddrs;
(host_for_lookup.as_str(), port)
.to_socket_addrs()
.map(|it| it.map(|s| s.ip()).collect::<Vec<_>>())
})
.await
{
Ok(Ok(v)) => v,
_ => {
deny(
&mut client,
audit,
&req.host,
req.port,
None,
"resolution failed",
)
.await;
return Err("resolution failed".to_string());
}
};
let decision = decide_connect(cfg, &req, &resolved);
let (host, port, approved_ip) = match decision {
ConnectDecision::Allow {
host,
port,
approved_ip,
} => (host, port, approved_ip),
ConnectDecision::Deny { reason } => {
deny(&mut client, audit, &req.host, req.port, None, &reason).await;
return Err(reason);
}
};
let upstream = match tokio::time::timeout(
cfg.limits.handshake_timeout,
TcpStream::connect(SocketAddr::new(approved_ip, port)),
)
.await
{
Ok(Ok(s)) => s,
_ => {
deny(
&mut client,
audit,
&host,
port,
Some(approved_ip),
"upstream connect failed",
)
.await;
return Err("upstream connect failed".to_string());
}
};
if client
.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
.await
.is_err()
{
return Err("client write failed".to_string());
}
let mut first = vec![0u8; 4096];
let n = match tokio::time::timeout(cfg.limits.handshake_timeout, client.read(&mut first)).await
{
Ok(Ok(n)) => n,
_ => {
let _ = client.shutdown().await;
emit_audit(
audit,
&host,
port,
Some(approved_ip),
false,
"no client hello",
0,
0,
);
return Err("no client hello".to_string());
}
};
first.truncate(n);
let sni = extract_sni(&first);
if !cfg.allow_plaintext {
if sni.is_none() && host.parse::<IpAddr>().is_err() {
let _ = client.shutdown().await;
emit_audit(
audit,
&host,
port,
Some(approved_ip),
false,
"non-TLS or SNI-less tunnel rejected",
0,
0,
);
return Err("non-TLS tunnel".to_string());
}
if !sni_matches_host(sni.as_deref(), &host) {
let _ = client.shutdown().await;
emit_audit(
audit,
&host,
port,
Some(approved_ip),
false,
"SNI does not match CONNECT host",
0,
0,
);
return Err("SNI mismatch".to_string());
}
}
let mut upstream = upstream;
if upstream.write_all(&first).await.is_err() {
let _ = client.shutdown().await;
return Err("upstream write failed".to_string());
}
let initial_up = first.len() as u64;
let (bytes_up, bytes_down) = tunnel(&mut client, &mut upstream, cfg, initial_up).await;
emit_audit(
audit,
&host,
port,
Some(approved_ip),
true,
"tunnel closed",
bytes_up,
bytes_down,
);
Ok(())
}
async fn read_request_head(client: &mut TcpStream) -> Result<String, String> {
const MAX_HEAD: usize = 8 * 1024;
let mut buf = Vec::with_capacity(1024);
let mut byte = [0u8; 1];
loop {
let n = client
.read(&mut byte)
.await
.map_err(|_| "read error".to_string())?;
if n == 0 {
return Err("connection closed before request head".to_string());
}
buf.push(byte[0]);
if buf.ends_with(b"\r\n\r\n") {
break;
}
if buf.len() > MAX_HEAD {
return Err("request head too large".to_string());
}
}
String::from_utf8(buf).map_err(|_| "request head not UTF-8".to_string())
}
async fn tunnel(
client: &mut TcpStream,
upstream: &mut TcpStream,
cfg: &BrokerConfig,
initial_up: u64,
) -> (u64, u64) {
let (mut cr, mut cw) = client.split();
let (mut ur, mut uw) = upstream.split();
let cap = cfg.limits.max_bytes_per_direction;
let idle = cfg.limits.idle_timeout;
let up = copy_capped(&mut cr, &mut uw, cap.saturating_sub(initial_up), idle);
let down = copy_capped(&mut ur, &mut cw, cap, idle);
let (up_n, down_n) = tokio::join!(up, down);
(initial_up + up_n, down_n)
}
async fn copy_capped<R, W>(from: &mut R, to: &mut W, cap: u64, idle: Duration) -> u64
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
let mut total = 0u64;
let mut buf = vec![0u8; 16 * 1024];
loop {
if total >= cap {
break;
}
let n = match tokio::time::timeout(idle, from.read(&mut buf)).await {
Ok(Ok(0)) => break,
Ok(Ok(n)) => n,
Ok(Err(_)) => break,
Err(_) => break, };
let remaining = (cap - total) as usize;
let take = n.min(remaining);
if to.write_all(&buf[..take]).await.is_err() {
break;
}
total += take as u64;
if take < n {
break;
}
}
let _ = to.shutdown().await;
total
}
async fn deny<F>(
client: &mut TcpStream,
audit: &Arc<F>,
host: &str,
port: u16,
ip: Option<IpAddr>,
reason: &str,
) where
F: Fn(BrokerAuditEvent) + Send + Sync + 'static,
{
let _ = client.write_all(b"HTTP/1.1 403 Forbidden\r\n\r\n").await;
let _ = client.shutdown().await;
emit_audit(audit, host, port, ip, false, reason, 0, 0);
}
#[allow(clippy::too_many_arguments)]
fn emit_audit<F>(
audit: &Arc<F>,
host: &str,
port: u16,
ip: Option<IpAddr>,
allowed: bool,
reason: &str,
bytes_up: u64,
bytes_down: u64,
) where
F: Fn(BrokerAuditEvent) + Send + Sync + 'static,
{
audit(BrokerAuditEvent {
host: host.to_string(),
port,
approved_ip: ip,
allowed,
reason: reason.to_string(),
bytes_up,
bytes_down,
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn allowlist(domains: &[&str], ports: &[u16]) -> NetworkPolicy {
NetworkPolicy::AllowListedDomains {
domains: domains.iter().map(|d| d.to_string()).collect(),
ports: ports.iter().copied().collect::<BTreeSet<u16>>(),
}
}
fn cfg_for(domains: &[&str]) -> BrokerConfig {
BrokerConfig::new(allowlist(domains, &[443]), "s3cr3t-token".to_string())
.expect("non-empty token")
}
fn req(host: &str, port: u16, token: Option<&str>) -> ConnectRequest {
ConnectRequest {
host: host.to_string(),
port,
bearer_token: token.map(|t| t.to_string()),
}
}
#[test]
fn parse_connect_basic() {
let head = "CONNECT pypi.org:443 HTTP/1.1\r\nHost: pypi.org:443\r\nProxy-Authorization: Bearer abc123\r\n\r\n";
let r = parse_connect_head(head).expect("parse");
assert_eq!(r.host, "pypi.org");
assert_eq!(r.port, 443);
assert_eq!(r.bearer_token.as_deref(), Some("abc123"));
}
#[test]
fn parse_connect_rejects_non_connect() {
let head = "GET / HTTP/1.1\r\n\r\n";
assert!(parse_connect_head(head).is_err());
}
#[test]
fn parse_connect_ipv6_authority() {
let head = "CONNECT [2606:4700::1111]:443 HTTP/1.1\r\n\r\n";
let r = parse_connect_head(head).expect("parse");
assert_eq!(r.host, "2606:4700::1111");
assert_eq!(r.port, 443);
assert!(r.bearer_token.is_none());
}
#[test]
fn parse_connect_requires_port() {
let head = "CONNECT pypi.org HTTP/1.1\r\n\r\n";
assert!(parse_connect_head(head).is_err());
}
#[test]
fn decide_rejects_missing_token() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 443, None),
&["93.184.216.34".parse().unwrap()],
);
match d {
ConnectDecision::Deny { reason } => assert!(reason.contains("token")),
_ => panic!("expected deny on missing token"),
}
}
#[test]
fn decide_rejects_wrong_token() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 443, Some("nope")),
&["93.184.216.34".parse().unwrap()],
);
assert!(matches!(d, ConnectDecision::Deny { .. }));
}
#[test]
fn decide_rejects_unlisted_domain() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("evil.example", 443, Some("s3cr3t-token")),
&["93.184.216.34".parse().unwrap()],
);
match d {
ConnectDecision::Deny { reason } => assert!(reason.contains("policy denies")),
_ => panic!("expected deny on unlisted domain"),
}
}
#[test]
fn decide_rejects_unlisted_port() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 80, Some("s3cr3t-token")),
&["93.184.216.34".parse().unwrap()],
);
assert!(matches!(d, ConnectDecision::Deny { .. }));
}
#[test]
fn decide_rejects_private_resolution() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 443, Some("s3cr3t-token")),
&["127.0.0.1".parse().unwrap(), "10.0.0.5".parse().unwrap()],
);
match d {
ConnectDecision::Deny { reason } => assert!(reason.contains("no public address")),
_ => panic!("expected deny on private resolution"),
}
}
#[test]
fn decide_rejects_metadata_resolution() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 443, Some("s3cr3t-token")),
&["169.254.169.254".parse().unwrap()],
);
assert!(matches!(d, ConnectDecision::Deny { .. }));
}
#[test]
fn decide_picks_first_public_ip() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 443, Some("s3cr3t-token")),
&[
"10.0.0.5".parse().unwrap(),
"93.184.216.34".parse().unwrap(),
],
);
match d {
ConnectDecision::Allow {
host,
port,
approved_ip,
} => {
assert_eq!(host, "pypi.org");
assert_eq!(port, 443);
assert_eq!(approved_ip, "93.184.216.34".parse::<IpAddr>().unwrap());
}
_ => panic!("expected allow"),
}
}
#[test]
fn decide_rejects_too_many_addresses() {
let mut cfg = cfg_for(&["pypi.org"]);
cfg.limits.max_resolved_addrs = 2;
let resolved: Vec<IpAddr> = vec![
"93.184.216.1".parse().unwrap(),
"93.184.216.2".parse().unwrap(),
"93.184.216.3".parse().unwrap(),
];
let d = decide_connect(&cfg, &req("pypi.org", 443, Some("s3cr3t-token")), &resolved);
match d {
ConnectDecision::Deny { reason } => assert!(reason.contains("addresses")),
_ => panic!("expected deny on fan-out"),
}
}
fn client_hello_with_sni(name: &str) -> Vec<u8> {
let name_bytes = name.as_bytes();
let mut sn_entry = Vec::new();
sn_entry.push(0u8); sn_entry.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
sn_entry.extend_from_slice(name_bytes);
let mut sn_list = Vec::new();
sn_list.extend_from_slice(&(sn_entry.len() as u16).to_be_bytes());
sn_list.extend_from_slice(&sn_entry);
let mut ext = Vec::new();
ext.extend_from_slice(&0u16.to_be_bytes()); ext.extend_from_slice(&(sn_list.len() as u16).to_be_bytes());
ext.extend_from_slice(&sn_list);
let mut body = Vec::new();
body.extend_from_slice(&[0x03, 0x03]); body.extend_from_slice(&[0u8; 32]); body.push(0u8); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&[0x13, 0x01]); body.push(1u8); body.push(0u8); body.extend_from_slice(&(ext.len() as u16).to_be_bytes()); body.extend_from_slice(&ext);
let mut hs = Vec::new();
hs.push(1u8); let blen = body.len();
hs.push(((blen >> 16) & 0xff) as u8);
hs.push(((blen >> 8) & 0xff) as u8);
hs.push((blen & 0xff) as u8);
hs.extend_from_slice(&body);
let mut rec = Vec::new();
rec.push(22u8); rec.extend_from_slice(&[0x03, 0x01]); rec.extend_from_slice(&(hs.len() as u16).to_be_bytes());
rec.extend_from_slice(&hs);
rec
}
#[test]
fn extract_sni_parses_hostname() {
let hello = client_hello_with_sni("pypi.org");
assert_eq!(extract_sni(&hello).as_deref(), Some("pypi.org"));
}
#[test]
fn extract_sni_lowercases_and_strips_dot() {
let hello = client_hello_with_sni("PyPI.ORG.");
assert_eq!(extract_sni(&hello).as_deref(), Some("pypi.org"));
}
#[test]
fn extract_sni_rejects_non_tls() {
let http = b"GET / HTTP/1.1\r\nHost: x\r\n\r\n";
assert!(extract_sni(http).is_none());
}
#[test]
fn extract_sni_handles_truncation() {
let mut hello = client_hello_with_sni("pypi.org");
hello.truncate(hello.len() / 2);
assert!(extract_sni(&hello).is_none());
}
#[test]
fn sni_matches_host_exact() {
assert!(sni_matches_host(Some("pypi.org"), "pypi.org"));
assert!(sni_matches_host(Some("PyPI.org."), "pypi.org"));
}
#[test]
fn sni_mismatch_is_rejected() {
assert!(!sni_matches_host(Some("attacker.example"), "pypi.org"));
}
#[test]
fn sni_missing_rejected_for_named_host() {
assert!(!sni_matches_host(None, "pypi.org"));
}
#[test]
fn sni_exempt_for_ip_literal_host() {
assert!(sni_matches_host(None, "93.184.216.34"));
}
#[test]
fn constant_time_eq_behaves() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
assert!(constant_time_eq(b"", b""));
assert!(!constant_time_eq(b"ab", b"abc"));
assert!(!constant_time_eq(b"", b"a"));
assert!(!constant_time_eq(b"a", b""));
let token = vec![0x5au8; 64];
let mut wrong = token.clone();
*wrong.last_mut().unwrap() ^= 0x01;
assert!(constant_time_eq(&token, &token));
assert!(!constant_time_eq(&token, &wrong));
assert!(!constant_time_eq(&token, &token[..32]));
}
#[test]
fn broker_config_rejects_empty_token() {
let err = BrokerConfig::new(allowlist(&["pypi.org"], &[443]), String::new())
.expect_err("empty token must be rejected at construction");
assert!(err.contains("must not be empty"));
}
#[test]
fn decide_denies_when_configured_token_is_empty() {
let cfg = BrokerConfig {
network: allowlist(&["pypi.org"], &[443]),
session_token: String::new(),
allow_plaintext: false,
limits: BrokerLimits::default(),
};
let d_empty = decide_connect(
&cfg,
&req("pypi.org", 443, Some("")),
&["93.184.216.34".parse().unwrap()],
);
assert!(
matches!(d_empty, ConnectDecision::Deny { .. }),
"empty configured + empty presented token must DENY, not authenticate"
);
let d_missing = decide_connect(
&cfg,
&req("pypi.org", 443, None),
&["93.184.216.34".parse().unwrap()],
);
assert!(matches!(d_missing, ConnectDecision::Deny { .. }));
}
#[test]
fn decide_denies_empty_presented_against_real_token() {
let cfg = cfg_for(&["pypi.org"]);
let d = decide_connect(
&cfg,
&req("pypi.org", 443, Some("")),
&["93.184.216.34".parse().unwrap()],
);
assert!(matches!(d, ConnectDecision::Deny { .. }));
}
#[test]
fn broker_state_caps_concurrency() {
let st = BrokerState::default();
assert!(st.try_acquire(2));
assert!(st.try_acquire(2));
assert!(!st.try_acquire(2));
assert_eq!(st.active(), 2);
st.release();
assert!(st.try_acquire(2));
}
#[test]
fn broker_config_is_tls_only_by_default() {
let cfg = cfg_for(&["pypi.org"]);
assert!(!cfg.allow_plaintext);
assert!(!cfg.network.is_deny_all());
}
}