use super::body::Exchange; use crate::{ runtime::App, server::{ConnectionLease, ServerShutdown}, }; use anyhow::{ensure, Result}; use axum::{body::Body, extract::Request}; use base64::{engine::general_purpose::STANDARD, Engine}; use bytes::{Buf, Bytes}; use h3::error::Code; use http::{HeaderMap, Method, Version}; use http_body::{Body as HttpBody, Frame, SizeHint}; use http_body_util::BodyExt; use hyper::upgrade::{OnUpgrade, Upgraded}; use hyper_util::rt::TokioIo; use rand::{rngs::SysRng, TryRng}; use serde::Serialize; use sha1::{Digest, Sha1}; use std::{ collections::BTreeMap, future::Future, io, pin::Pin, sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, Mutex, Weak, }, task::{Context, Poll}, time::{Duration, Instant}, }; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; use tokio_util::sync::CancellationToken; fn has_token(headers: &HeaderMap, name: &str, token: &str) -> bool { headers .get_all(name) .iter() .filter_map(|v| v.to_str().ok()) .flat_map(|v| v.split(',')) .any(|v| v.trim().eq_ignore_ascii_case(token)) } fn single<'a>(headers: &'a HeaderMap, name: &str) -> Result<&'a str> { ensure!( headers.get_all(name).iter().count() == 1, "WebSocket header must occur once" ); Ok(headers .get(name) .ok_or_else(|| anyhow::anyhow!("missing WebSocket header"))? .to_str()?) } pub struct Handshake { pub accept: String, upstream_key: Option, protocols: Vec, bootstrap: Bootstrap, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Bootstrap { Http1, Http2, Http3, } impl Handshake { pub fn h1(&self) -> bool { self.bootstrap == Bootstrap::Http1 } pub fn h2(&self) -> bool { self.bootstrap == Bootstrap::Http2 } pub fn h3(&self) -> bool { self.bootstrap == Bootstrap::Http3 } pub fn upstream_key(&self) -> Option<&str> { self.upstream_key.as_deref() } } fn protocols(headers: &HeaderMap) -> Result> { let protocols = headers .get_all("sec-websocket-protocol") .iter() .map(|v| v.to_str()) .collect::, _>>()? .into_iter() .flat_map(|v| v.split(',')) .map(|v| v.trim().to_owned()) .collect::>(); ensure!( protocols.len() <= 32 && protocols.iter().all(|v| !v.is_empty()), "invalid WebSocket protocols" ); Ok(protocols) } fn accept_for_key(key: &str) -> String { let mut hasher = Sha1::new(); hasher.update(key.as_bytes()); hasher.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); STANDARD.encode(hasher.finalize()) } pub fn validate(request: &Request) -> Result { ensure!( request.method() == Method::GET && request.version() == Version::HTTP_11, "WebSocket requires HTTP/1.1 GET" ); ensure!( single(request.headers(), "upgrade")?.eq_ignore_ascii_case("websocket") && has_token(request.headers(), "connection", "upgrade"), "invalid WebSocket upgrade" ); ensure!( single(request.headers(), "sec-websocket-version")? == "13", "WebSocket version must be 13" ); let key = single(request.headers(), "sec-websocket-key")?; ensure!( key.len() == 24 && STANDARD.decode(key)?.len() == 16, "invalid WebSocket key" ); ensure!( !request.headers().contains_key("transfer-encoding"), "WebSocket handshake cannot contain a transfer body" ); ensure!( request.headers().get_all("content-length").iter().count() <= 1 && request .headers() .get("content-length") .is_none_or(|v| v == "0"), "WebSocket handshake cannot contain a body" ); for value in request.headers().get_all("connection").iter() { for field in value.to_str()?.split(',') { ensure!( !field .trim() .to_ascii_lowercase() .starts_with("sec-websocket-"), "Connection must not nominate WebSocket handshake fields" ); } } Ok(Handshake { accept: accept_for_key(key), upstream_key: None, protocols: protocols(request.headers())?, bootstrap: Bootstrap::Http1, }) } /// Validate the RFC 8441 bootstrap used by RFC 9220 on HTTP/3. The /// Sec-WebSocket-Key/Accept proof belongs only to the HTTP/1.1 hop, so a fresh /// key is synthesized later for the backend Upgrade request. pub fn validate_h3(request: &Request) -> Result { ensure!( request.method() == Method::CONNECT && request.version() == Version::HTTP_3, "HTTP/3 WebSocket requires Extended CONNECT" ); ensure!( request .extensions() .get::() .is_some_and(|protocol| protocol.as_str() == "websocket"), "Extended CONNECT protocol is not websocket" ); for name in [ "connection", "upgrade", "sec-websocket-key", "sec-websocket-accept", "transfer-encoding", "content-length", ] { ensure!( !request.headers().contains_key(name), "HTTP/1.1-only WebSocket field on HTTP/3" ); } ensure!( single(request.headers(), "sec-websocket-version")? == "13", "WebSocket version must be 13" ); let mut nonce = [0u8; 16]; SysRng.try_fill_bytes(&mut nonce).expect("secure operating system random source unavailable"); let key = STANDARD.encode(nonce); Ok(Handshake { accept: accept_for_key(&key), upstream_key: Some(key), protocols: protocols(request.headers())?, bootstrap: Bootstrap::Http3, }) } /// Validate RFC 8441 Extended CONNECT on HTTP/2. The downstream hop does not /// use Sec-WebSocket-Key/Accept; a fresh RFC 6455 key is generated only when /// RustProxy has to bridge the stream to an HTTP/1.1 backend. pub fn validate_h2(request: &Request) -> Result { ensure!( request.method() == Method::CONNECT && request.version() == Version::HTTP_2, "HTTP/2 WebSocket requires Extended CONNECT" ); ensure!( request .extensions() .get::() .is_some_and(|protocol| protocol.as_str() == "websocket"), "Extended CONNECT protocol is not websocket" ); for name in [ "connection", "upgrade", "sec-websocket-key", "sec-websocket-accept", "transfer-encoding", "content-length", ] { ensure!( !request.headers().contains_key(name), "HTTP/1.1-only WebSocket field on HTTP/2" ); } ensure!( single(request.headers(), "sec-websocket-version")? == "13", "WebSocket version must be 13" ); let mut nonce = [0u8; 16]; SysRng.try_fill_bytes(&mut nonce).expect("secure operating system random source unavailable"); let key = STANDARD.encode(nonce); Ok(Handshake { accept: accept_for_key(&key), upstream_key: Some(key), protocols: protocols(request.headers())?, bootstrap: Bootstrap::Http2, }) } pub fn h2_protocol(request: &Request) -> Option<&str> { request .extensions() .get::() .map(|protocol| protocol.as_str()) } pub fn h3_protocol(request: &Request) -> Option<&str> { request .extensions() .get::() .map(|protocol| protocol.as_str()) } pub fn validate_response( headers: &HeaderMap, expected: &Handshake, legacy_support: bool, ) -> Result<()> { if legacy_support { // Some older embedded servers return a valid 101 + Sec-WebSocket-Accept // while omitting Connection: Upgrade, duplicating Upgrade, or attaching // zero/meaningless HTTP body-framing headers. The connection is dedicated // to the upgraded tunnel and those framing headers are stripped before the // downstream response, so keep the cryptographic accept proof strict while // tolerating the legacy response syntax. ensure!( has_token(headers, "upgrade", "websocket"), "invalid legacy upstream upgrade" ); if headers.contains_key("content-length") { for value in headers.get_all("content-length").iter() { ensure!( value.to_str()?.trim() == "0", "legacy 101 response declared a non-zero body" ); } } } else { ensure!( single(headers, "upgrade")?.eq_ignore_ascii_case("websocket") && has_token(headers, "connection", "upgrade"), "invalid upstream upgrade" ); ensure!( !headers.contains_key("transfer-encoding"), "101 response must not use Transfer-Encoding" ); ensure!( headers.get_all("content-length").iter().count() <= 1 && headers .get("content-length") .is_none_or(|value| value.to_str().is_ok_and(|value| value.trim() == "0")), "101 response Content-Length must be 0 when present" ); for value in headers.get_all("connection").iter() { for field in value.to_str()?.split(',') { ensure!( !field .trim() .to_ascii_lowercase() .starts_with("sec-websocket-"), "upstream nominated a protected handshake field" ); } } } ensure!( single(headers, "sec-websocket-accept")? == expected.accept, "upstream WebSocket accept mismatch" ); if headers.contains_key("sec-websocket-protocol") { let selected = single(headers, "sec-websocket-protocol")?; ensure!( expected.protocols.iter().any(|v| v == selected), "upstream chose an unoffered subprotocol" ); } Ok(()) } pub fn validate_h2_response(headers: &HeaderMap, expected: &Handshake) -> Result<()> { for name in [ "connection", "upgrade", "sec-websocket-key", "sec-websocket-accept", "transfer-encoding", "content-length", ] { ensure!( !headers.contains_key(name), "HTTP/1.1-only WebSocket field on HTTP/2 response" ); } if headers.contains_key("sec-websocket-protocol") { let selected = single(headers, "sec-websocket-protocol")?; ensure!( expected.protocols.iter().any(|value| value == selected), "upstream chose an unoffered subprotocol" ); } Ok(()) } struct MeteredIo { inner: T, exchange: Arc, session: Arc, client_to_backend: bool, started: Instant, activity: Arc, } fn record_bytes( exchange: &Exchange, bytes: u64, client_to_backend: bool, activity: &AtomicU64, started: Instant, ) { if bytes == 0 { return; } exchange.backend_bytes(bytes, client_to_backend); activity.store(started.elapsed().as_millis() as u64, Ordering::Relaxed); if client_to_backend { exchange .metrics .websocket_bytes_in .fetch_add(bytes, Ordering::Relaxed); exchange.request_bytes.fetch_add(bytes, Ordering::Relaxed); } else { exchange .metrics .websocket_bytes_out .fetch_add(bytes, Ordering::Relaxed); exchange.response_bytes.fetch_add(bytes, Ordering::Relaxed); } } impl AsyncRead for MeteredIo { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let this = self.get_mut(); let before = buf.filled().len(); let result = Pin::new(&mut this.inner).poll_read(cx, buf); if let Poll::Ready(Ok(())) = &result { let bytes = (buf.filled().len() - before) as u64; record_bytes( &this.exchange, bytes, this.client_to_backend, &this.activity, this.started, ); this.session.record(bytes, this.client_to_backend); } result } } impl AsyncWrite for MeteredIo { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, bytes: &[u8], ) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_write(cx, bytes) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_flush(cx) } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_shutdown(cx) } } #[derive(Debug, Clone, Copy)] enum TunnelTransport { H1, H2, H3, }