2026-02-16 11:21:44 -07:00
|
|
|
//! HTTP protocol implementation for tree communication.
|
|
|
|
|
//!
|
|
|
|
|
//! This protocol wraps data in HTTP requests/responses for traffic blending.
|
|
|
|
|
|
2026-02-16 12:52:46 -07:00
|
|
|
use super::stack::{HttpConfig, Protocol, ProtocolError};
|
2026-02-16 11:21:44 -07:00
|
|
|
use serde_json::Value;
|
|
|
|
|
|
|
|
|
|
/// HTTP protocol for tree communication.
|
|
|
|
|
///
|
|
|
|
|
/// Wraps outgoing data in HTTP requests and parses incoming HTTP responses.
|
|
|
|
|
pub struct HttpProtocol {
|
|
|
|
|
config: HttpConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HttpProtocol {
|
|
|
|
|
pub fn new(config: HttpConfig) -> Self {
|
|
|
|
|
Self { config }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build HTTP request
|
|
|
|
|
fn build_request(&self, body: &[u8]) -> Vec<u8> {
|
|
|
|
|
let body_len = body.len();
|
|
|
|
|
let body_str = String::from_utf8_lossy(body);
|
|
|
|
|
|
|
|
|
|
let mut request = format!(
|
|
|
|
|
"{} {} HTTP/1.1\r\n\
|
|
|
|
|
Host: {}\r\n\
|
|
|
|
|
User-Agent: {}\r\n\
|
|
|
|
|
Content-Type: application/json\r\n\
|
|
|
|
|
Content-Length: {}\r\n",
|
|
|
|
|
self.config.method,
|
|
|
|
|
self.config.path,
|
|
|
|
|
"localhost", // Would be configured in production
|
|
|
|
|
self.config.user_agent,
|
|
|
|
|
body_len
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Add custom headers
|
|
|
|
|
for (key, value) in &self.config.headers {
|
|
|
|
|
request.push_str(&format!("{}: {}\r\n", key, value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request.push_str("\r\n");
|
|
|
|
|
request.push_str(&body_str);
|
|
|
|
|
|
|
|
|
|
request.into_bytes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse HTTP response and extract body
|
|
|
|
|
fn parse_response(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
|
|
|
|
|
let data_str = String::from_utf8(data.to_vec())
|
|
|
|
|
.map_err(|e| ProtocolError::DecodeError(e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
// Find body start (after \r\n\r\n)
|
|
|
|
|
let body_start = match data_str.find("\r\n\r\n") {
|
|
|
|
|
Some(pos) => pos + 4,
|
|
|
|
|
None => {
|
|
|
|
|
return Err(ProtocolError::DecodeError(
|
|
|
|
|
"Invalid HTTP response".to_string(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract status code
|
|
|
|
|
let status_line = data_str
|
|
|
|
|
.split("\r\n")
|
|
|
|
|
.next()
|
|
|
|
|
.ok_or_else(|| ProtocolError::DecodeError("No status line".to_string()))?;
|
|
|
|
|
|
|
|
|
|
let status_code: u16 = status_line
|
|
|
|
|
.split_whitespace()
|
|
|
|
|
.nth(1)
|
|
|
|
|
.and_then(|s| s.parse().ok())
|
|
|
|
|
.ok_or_else(|| ProtocolError::DecodeError("Invalid status code".to_string()))?;
|
|
|
|
|
|
|
|
|
|
if status_code < 200 || status_code >= 300 {
|
|
|
|
|
return Err(ProtocolError::DecodeError(format!(
|
|
|
|
|
"HTTP error: {}",
|
|
|
|
|
status_code
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract body
|
|
|
|
|
let body = &data_str[body_start..];
|
|
|
|
|
Ok(body.as_bytes().to_vec())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Protocol for HttpProtocol {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"http"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn encode(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
|
|
|
|
|
Ok(self.build_request(data))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decode(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
|
|
|
|
|
self.parse_response(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn status(&self) -> Value {
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"protocol": "http",
|
|
|
|
|
"method": self.config.method,
|
|
|
|
|
"path": self.config.path,
|
|
|
|
|
"headers": self.config.headers,
|
|
|
|
|
"user_agent": self.config.user_agent,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// HTTP server for receiving tree messages.
|
|
|
|
|
///
|
|
|
|
|
/// This is a simple implementation for testing - in production you'd
|
|
|
|
|
/// use a proper HTTP server.
|
|
|
|
|
pub struct HttpServer {
|
|
|
|
|
config: HttpConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl HttpServer {
|
|
|
|
|
pub fn new(config: HttpConfig) -> Self {
|
|
|
|
|
Self { config }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parse incoming HTTP request and extract body
|
|
|
|
|
pub fn parse_request(&self, data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
|
|
|
|
|
let data_str = String::from_utf8(data.to_vec())
|
|
|
|
|
.map_err(|e| ProtocolError::DecodeError(e.to_string()))?;
|
|
|
|
|
|
|
|
|
|
// Find body start
|
|
|
|
|
let body_start = match data_str.find("\r\n\r\n") {
|
|
|
|
|
Some(pos) => pos + 4,
|
|
|
|
|
None => {
|
|
|
|
|
return Err(ProtocolError::DecodeError(
|
|
|
|
|
"Invalid HTTP request".to_string(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Extract Content-Length
|
|
|
|
|
let mut content_length = 0;
|
|
|
|
|
for line in data_str.lines() {
|
|
|
|
|
if line.to_lowercase().starts_with("content-length:") {
|
|
|
|
|
content_length = line
|
|
|
|
|
.split(':')
|
|
|
|
|
.nth(1)
|
|
|
|
|
.and_then(|s| s.trim().parse().ok())
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let body = &data_str[body_start..];
|
|
|
|
|
if body.len() >= content_length {
|
|
|
|
|
Ok(body[..content_length].as_bytes().to_vec())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(body.as_bytes().to_vec())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build HTTP response
|
|
|
|
|
pub fn build_response(&self, body: &[u8], status: u16) -> Vec<u8> {
|
|
|
|
|
let body_len = body.len();
|
|
|
|
|
let body_str = String::from_utf8_lossy(body);
|
|
|
|
|
|
|
|
|
|
let status_text = match status {
|
|
|
|
|
200 => "OK",
|
|
|
|
|
400 => "Bad Request",
|
|
|
|
|
404 => "Not Found",
|
|
|
|
|
500 => "Internal Server Error",
|
|
|
|
|
_ => "Unknown",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
format!(
|
|
|
|
|
"HTTP/1.1 {} {}\r\n\
|
|
|
|
|
Content-Type: application/json\r\n\
|
|
|
|
|
Content-Length: {}\r\n\
|
|
|
|
|
Connection: close\r\n\
|
|
|
|
|
\r\n\
|
|
|
|
|
{}",
|
|
|
|
|
status, status_text, body_len, body_str
|
|
|
|
|
)
|
|
|
|
|
.into_bytes()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for HttpServer {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self::new(Default::default())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_http_encode_decode() {
|
|
|
|
|
let proto = HttpProtocol::new(Default::default());
|
|
|
|
|
|
|
|
|
|
let data = r#"{"action": "test", "data": "hello"}"#.as_bytes();
|
|
|
|
|
let encoded = proto.encode(data).unwrap();
|
|
|
|
|
|
|
|
|
|
// Build a valid response
|
|
|
|
|
let response = b"HTTP/1.1 200 OK\r\nContent-Length: 29\r\n\r\n{\"action\": \"test\", \"data\": \"hello\"}";
|
|
|
|
|
|
|
|
|
|
let decoded = proto.decode(response).unwrap();
|
|
|
|
|
assert_eq!(decoded, data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_request_building() {
|
|
|
|
|
let server = HttpServer::new(Default::default());
|
|
|
|
|
|
|
|
|
|
let body = r#"{"test": "data"}"#.as_bytes();
|
|
|
|
|
let request = server.build_request(body);
|
|
|
|
|
|
|
|
|
|
let request_str = String::from_utf8(request).unwrap();
|
|
|
|
|
assert!(request_str.contains("POST / HTTP/1.1"));
|
|
|
|
|
assert!(request_str.contains("Content-Length: 16"));
|
|
|
|
|
}
|
|
|
|
|
}
|