use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::time::Duration;
use tracing::{debug, warn};
pub const UPGRADE_WEBSOCKET: &str = "websocket";
pub const UPGRADE_H2C: &str = "h2c";
#[derive(Debug, Clone, PartialEq)]
pub enum UpgradeRequest {
WebSocket {
key: String,
version: String,
protocol: Option<String>,
},
H2c,
None,
}
pub fn parse_upgrade(headers: &[u8]) -> UpgradeRequest {
let header_str = std::str::from_utf8(headers).unwrap_or("");
if !header_str.contains("Upgrade:") {
return UpgradeRequest::None;
}
if header_str.contains("Upgrade: websocket") || header_str.contains("upgrade: websocket") {
let key = extract_header(header_str, "Sec-WebSocket-Key")
.unwrap_or("")
.to_string();
let version = extract_header(header_str, "Sec-WebSocket-Version")
.unwrap_or("13")
.to_string();
let protocol = extract_header(header_str, "Sec-WebSocket-Protocol");
return UpgradeRequest::WebSocket { key, version, protocol };
}
if header_str.contains("Upgrade: h2c") {
return UpgradeRequest::H2c;
}
UpgradeRequest::None
}
fn extract_header<'a>(headers: &'a str, name: &str) -> Option<&'a str> {
for line in headers.lines() {
let lower = line.to_lowercase();
if lower.starts_with(&name.to_lowercase()) {
let val = line.splitn(2, ':').nth(1)?.trim();
return Some(val);
}
}
None
}
pub fn ws_upgrade_response(key: &str) -> Vec<u8> {
use sha1::{Digest, Sha1};
const WS_GUID: &str = "258EAFA5-E914-47DA-95CA-5AB5DC11B725";
let mut hasher = Sha1::new();
hasher.update(key.as_bytes());
hasher.update(WS_GUID.as_bytes());
let accept = base64::encode(hasher.finalize());
format!(
"HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: {accept}\r\n\
\r\n"
)
.into_bytes()
}
pub struct ConnectionUpgrader {
stream: Option<TcpStream>,
read_buf: Vec<u8>,
}
impl ConnectionUpgrader {
pub fn new(stream: TcpStream) -> Self {
Self {
stream: Some(stream),
read_buf: Vec::new(),
}
}
pub async fn try_upgrade(mut self) -> Result<UpgradeResult, std::io::Error> {
let mut stream = self.stream.take().expect("stream already consumed");
let mut buf = vec![0u8; 2048];
let n = stream.peek(&mut buf).await?;
buf.truncate(n);
let upgrade = parse_upgrade(&buf);
match upgrade {
UpgradeRequest::WebSocket { key, version: _, protocol: _ } => {
debug!("WebSocket upgrade requested (key={})", &key[..16]);
let response = ws_upgrade_response(&key);
stream.write_all(&response).await?;
let std_stream = stream.into_std()?;
let raw_fd = std_stream.into_raw_fd();
tokio::task::spawn_blocking(move || {
unsafe { ws_frame_loop(raw_fd) }
});
Ok(UpgradeResult::Upgraded("websocket"))
}
UpgradeRequest::H2c => {
debug!("h2c upgrade requested");
let std_stream = stream.into_std()?;
let raw_fd = std_stream.into_raw_fd();
tokio::task::spawn_blocking(move || {
unsafe { h2c_worker(raw_fd) }
});
Ok(UpgradeResult::Upgraded("h2c"))
}
UpgradeRequest::None => {
self.stream = Some(stream);
Ok(UpgradeResult::Passthrough(self))
}
}
}
pub fn into_stream(mut self) -> Option<TcpStream> {
self.stream.take()
}
pub fn buffered_bytes(&self) -> &[u8] {
&self.read_buf
}
}
#[derive(Debug)]
pub enum UpgradeResult<'a> {
Upgraded(&'a str),
Passthrough(ConnectionUpgrader),
}
unsafe fn ws_frame_loop(fd: i32) {
let mut stream = std::net::TcpStream::from_raw_fd(fd);
let _ = stream.set_read_timeout(Some(Duration::from_secs(300)));
let _ = stream.set_write_timeout(Some(Duration::from_secs(5)));
debug!("WebSocket frame loop started on fd={}", fd);
let mut buf = [0u8; 8192];
loop {
match stream.read(&mut buf) {
Ok(0) => {
debug!("ws frame loop: connection closed");
break;
}
Ok(n) => {
if n >= 2 {
let opcode = buf[0] & 0x0f;
let masked = (buf[1] & 0x80) != 0;
let mut payload_len = (buf[1] & 0x7f) as usize;
let offset = if payload_len == 126 { 4 } else if payload_len == 127 { 10 } else { 2 };
if masked { }
match opcode {
0x1 | 0x2 => {
let response = vec![0x81; 1]; let _ = stream.write_all(&buf[..n]);
}
0x8 => {
debug!("ws: close frame received");
break;
}
0x9 => {
let mut pong = vec![0x8a];
pong.extend_from_slice(&buf[2..n]);
let _ = stream.write_all(&pong);
}
_ => {}
}
}
}
Err(e) => {
warn!("ws frame loop error: {e}");
break;
}
}
}
debug!("ws frame loop exited (fd={})", fd);
}
unsafe fn h2c_worker(fd: i32) {
let _ = fd;
debug!("h2c worker stub — implement HTTP/2 framing");
}
pub fn set_keepalive(stream: &TcpStream) -> std::io::Result<()> {
let sock_ref = socket2::SockRef::from(stream);
sock_ref.set_tcp_keepalive(
&socket2::TcpKeepalive::new().with_time(Duration::from_secs(30)),
)
}
pub fn leak_raw_fd(stream: TcpStream) -> i32 {
stream.into_raw_fd()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ws_upgrade() {
let headers = b"GET /a2a/ws HTTP/1.1\r\n\
Host: localhost:8787\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
Sec-WebSocket-Version: 13\r\n\r\n";
let upgrade = parse_upgrade(headers);
assert!(matches!(upgrade, UpgradeRequest::WebSocket { .. }));
if let UpgradeRequest::WebSocket { key, version, protocol: _ } = upgrade {
assert_eq!(key, "dGhlIHNhbXBsZSBub25jZQ==");
assert_eq!(version, "13");
}
}
#[test]
fn test_parse_no_upgrade() {
let headers = b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n";
assert_eq!(parse_upgrade(headers), UpgradeRequest::None);
}
#[test]
fn test_parse_h2c_upgrade() {
let headers = b"GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: h2c\r\n\r\n";
assert_eq!(parse_upgrade(headers), UpgradeRequest::H2c);
}
#[test]
fn test_ws_upgrade_response() {
let key = "dGhlIHNhbXBsZSBub25jZQ==";
let resp = ws_upgrade_response(key);
let resp_str = std::str::from_utf8(&resp).unwrap();
assert!(resp_str.contains("101 Switching Protocols"));
assert!(resp_str.contains("Sec-WebSocket-Accept:"));
assert!(resp_str.contains("s3pPLMBiTxaQ9kYGzzhZRbK+xOo="));
}
#[test]
fn test_extract_header_case_insensitive() {
let headers = "Content-Type: application/json\nX-API-Key: secret\n";
assert_eq!(extract_header(headers, "content-type"), Some("application/json"));
assert_eq!(extract_header(headers, "x-api-key"), Some("secret"));
assert_eq!(extract_header(headers, "missing"), None);
}
}