use crate::error::AppError;
use crate::fs::FileDetails;
use crate::response::create_error_response;
use crate::router::Router;
use log::{debug, error, info, trace, warn};
use rustls;
use std::collections::HashMap;
use std::fs::File;
use std::io::prelude::*;
use std::net::TcpStream;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
const MAX_REQUEST_BODY_SIZE: usize = 10 * 1024 * 1024 * 1024;
const MAX_HEADERS_SIZE: usize = 8 * 1024;
pub const STREAM_TO_DISK_THRESHOLD: usize = 64 * 1024 * 1024;
pub enum ClientStream {
Plain(TcpStream),
Tls(Box<rustls::StreamOwned<rustls::ServerConnection, TcpStream>>),
}
impl ClientStream {
pub fn peer_addr(&self) -> std::io::Result<std::net::SocketAddr> {
match self {
ClientStream::Plain(s) => s.peer_addr(),
ClientStream::Tls(s) => s.sock.peer_addr(),
}
}
pub fn set_read_timeout(&self, dur: Option<Duration>) -> std::io::Result<()> {
match self {
ClientStream::Plain(s) => s.set_read_timeout(dur),
ClientStream::Tls(s) => s.sock.set_read_timeout(dur),
}
}
}
impl std::io::Read for ClientStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
ClientStream::Plain(s) => s.read(buf),
ClientStream::Tls(s) => s.read(buf),
}
}
}
impl std::io::Write for ClientStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
ClientStream::Plain(s) => s.write(buf),
ClientStream::Tls(s) => s.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
ClientStream::Plain(s) => s.flush(),
ClientStream::Tls(s) => s.flush(),
}
}
}
#[derive(Debug)]
pub struct Request {
pub method: String,
pub path: String,
pub headers: HashMap<String, String>,
pub body: Option<RequestBody>,
}
#[derive(Debug)]
pub enum RequestBody {
Memory(Vec<u8>),
File { path: PathBuf, size: u64 },
}
impl RequestBody {
pub fn len(&self) -> usize {
let size = match self {
RequestBody::Memory(data) => data.len(),
RequestBody::File { size, .. } => *size as usize,
};
trace!("RequestBody size: {} bytes", size);
size
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct Response {
pub status_code: u16,
pub status_text: String,
pub headers: HashMap<String, String>,
pub body: ResponseBody,
}
pub enum ResponseBody {
Text(String),
StaticText(&'static str),
Binary(Vec<u8>),
StaticBinary(&'static [u8]),
Stream(FileDetails),
}
impl Request {
fn is_valid_http_method(method: &str) -> bool {
matches!(
method,
"GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH" | "TRACE" | "CONNECT"
)
}
pub fn from_stream(stream: &mut ClientStream) -> Result<Self, AppError> {
trace!("Starting HTTP request parsing from stream");
stream.set_read_timeout(Some(std::time::Duration::from_secs(30)))?;
let (headers_data, remaining_bytes) = Self::read_headers_with_remaining(stream)?;
debug!(
"Received headers ({} bytes), remaining buffer: {} bytes",
headers_data.len(),
remaining_bytes.len()
);
let mut lines = headers_data.lines();
let request_line = lines.next().ok_or(AppError::BadRequest)?;
trace!("Request line: {}", request_line);
let parts: Vec<&str> = request_line.split_whitespace().collect();
if parts.len() != 3 {
debug!("Invalid request line format: {}", request_line);
return Err(AppError::BadRequest);
}
let method = parts[0].to_string();
let raw_path = parts[1];
let version = parts[2];
if !Self::is_valid_http_method(&method) {
debug!("Invalid HTTP method: {}", method);
return Err(AppError::BadRequest);
}
if raw_path.contains('\0') || raw_path.is_empty() {
debug!("Invalid path: contains null byte or is empty");
return Err(AppError::BadRequest);
}
let path = Self::decode_url(raw_path)?;
debug!("Parsed request: {} {}", method, path);
trace!("Raw path before decoding: {}", raw_path);
trace!("HTTP version: {}", version);
if !version.starts_with("HTTP/1.") {
return Err(AppError::BadRequest);
}
let mut headers = HashMap::new();
for line in lines {
let line = line.trim();
if line.is_empty() {
break;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim().to_lowercase();
let value = value.trim().to_string();
trace!("Header: {} = {}", key, value);
if let Some(existing) = headers.get(&key) {
headers.insert(key, format!("{existing}, {value}"));
} else {
headers.insert(key, value);
}
}
}
let body = Self::read_request_body(stream, &headers, remaining_bytes)?;
if let Some(ref body) = body {
debug!("Request body parsed: {} bytes", body.len());
match body {
RequestBody::Memory(_) => trace!("Body stored in memory"),
RequestBody::File { path, size } => {
trace!("Body streamed to file: {} ({} bytes)", path.display(), size);
}
}
} else {
trace!("No request body");
}
debug!(
"Parsed request: {} {} (headers: {}, body_size: {})",
method,
path,
headers.len(),
body.as_ref().map(|b| b.len()).unwrap_or(0)
);
Ok(Request {
method,
path,
headers,
body,
})
}
fn read_headers_with_remaining(
stream: &mut ClientStream,
) -> Result<(String, Vec<u8>), AppError> {
let mut buffer = vec![0; MAX_HEADERS_SIZE];
let mut total_read = 0;
loop {
match stream.read(&mut buffer[total_read..]) {
Ok(0) => {
if total_read == 0 {
return Err(AppError::BadRequest);
}
break;
}
Ok(bytes_read) => {
total_read += bytes_read;
let double_crlf = b"\r\n\r\n";
let double_lf = b"\n\n";
if let Some(pos) = buffer[0..total_read]
.windows(4)
.position(|window| window == double_crlf)
{
let headers_end = pos;
let body_start = pos + 4;
match std::str::from_utf8(&buffer[0..headers_end]) {
Ok(headers_data) => {
let remaining_bytes = buffer[body_start..total_read].to_vec();
return Ok((headers_data.to_string(), remaining_bytes));
}
Err(_) => {
return Err(AppError::BadRequest);
}
}
} else if let Some(pos) = buffer[0..total_read]
.windows(2)
.position(|window| window == double_lf)
{
let headers_end = pos;
let body_start = pos + 2;
match std::str::from_utf8(&buffer[0..headers_end]) {
Ok(headers_data) => {
let remaining_bytes = buffer[body_start..total_read].to_vec();
return Ok((headers_data.to_string(), remaining_bytes));
}
Err(_) => {
return Err(AppError::BadRequest);
}
}
}
if total_read >= buffer.len() {
return Err(AppError::BadRequest);
}
}
Err(e) => return Err(AppError::Io(e)),
}
}
match std::str::from_utf8(&buffer[0..total_read]) {
Ok(data) => Ok((data.to_string(), Vec::new())),
Err(_) => Err(AppError::BadRequest),
}
}
fn read_request_body(
stream: &mut ClientStream,
headers: &HashMap<String, String>,
remaining_bytes: Vec<u8>,
) -> Result<Option<RequestBody>, AppError> {
let content_length = match headers.get("content-length") {
Some(length_str) => match length_str.parse::<usize>() {
Ok(length) => length,
Err(_) => return Err(AppError::BadRequest),
},
None => {
if let Some(encoding) = headers.get("transfer-encoding")
&& encoding.to_lowercase().contains("chunked")
{
warn!("Chunked transfer encoding not yet supported");
return Err(AppError::BadRequest);
}
return Ok(None);
}
};
if content_length == 0 {
return Ok(Some(RequestBody::Memory(Vec::new())));
}
if content_length > MAX_REQUEST_BODY_SIZE {
return Err(AppError::PayloadTooLarge(MAX_REQUEST_BODY_SIZE as u64));
}
if content_length <= STREAM_TO_DISK_THRESHOLD {
Self::read_body_to_memory(stream, content_length, remaining_bytes)
.map(|body| Some(RequestBody::Memory(body)))
} else {
Self::read_body_to_disk(stream, content_length, remaining_bytes)
.map(|(path, size)| Some(RequestBody::File { path, size }))
}
}
fn read_body_to_memory(
stream: &mut ClientStream,
content_length: usize,
remaining_bytes: Vec<u8>,
) -> Result<Vec<u8>, AppError> {
let mut body = Vec::with_capacity(content_length);
let bytes_from_headers = remaining_bytes.len().min(content_length);
body.extend_from_slice(&remaining_bytes[..bytes_from_headers]);
let bytes_needed = content_length - bytes_from_headers;
if bytes_needed > 0 {
let mut bytes_read = 0;
let chunk_size = 8192; let mut buffer = vec![0; chunk_size];
while bytes_read < bytes_needed {
let to_read = (bytes_needed - bytes_read).min(chunk_size);
match stream.read(&mut buffer[..to_read]) {
Ok(0) => {
return Err(AppError::BadRequest);
}
Ok(n) => {
body.extend_from_slice(&buffer[..n]);
bytes_read += n;
}
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut {
warn!("Request body read timeout");
}
return Err(AppError::Io(e));
}
}
}
}
if body.len() != content_length {
return Err(AppError::BadRequest);
}
debug!(
"Successfully read request body to memory: {} bytes",
body.len()
);
Ok(body)
}
fn read_body_to_disk(
stream: &mut ClientStream,
content_length: usize,
remaining_bytes: Vec<u8>,
) -> Result<(PathBuf, u64), AppError> {
let temp_filename = format!(
"irondrop_request_{}_{:x}.tmp",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join(&temp_filename);
let mut temp_file = File::create(&temp_path).map_err(|e| {
error!("Failed to create temporary file {temp_path:?}: {e}");
AppError::from(e)
})?;
let mut total_written = 0;
if !remaining_bytes.is_empty() {
let bytes_to_write = remaining_bytes.len().min(content_length);
temp_file
.write_all(&remaining_bytes[..bytes_to_write])
.map_err(|e| {
let _ = std::fs::remove_file(&temp_path);
AppError::from(e)
})?;
total_written += bytes_to_write;
}
let bytes_needed = content_length - total_written;
if bytes_needed > 0 {
let mut bytes_read = 0;
let chunk_size = 64 * 1024; let mut buffer = vec![0; chunk_size];
while bytes_read < bytes_needed {
let to_read = (bytes_needed - bytes_read).min(chunk_size);
match stream.read(&mut buffer[..to_read]) {
Ok(0) => {
let _ = std::fs::remove_file(&temp_path);
return Err(AppError::BadRequest);
}
Ok(n) => {
temp_file.write_all(&buffer[..n]).map_err(|e| {
let _ = std::fs::remove_file(&temp_path);
AppError::from(e)
})?;
bytes_read += n;
total_written += n;
}
Err(e) => {
let _ = std::fs::remove_file(&temp_path);
if e.kind() == std::io::ErrorKind::TimedOut {
warn!("Request body read timeout");
}
return Err(AppError::Io(e));
}
}
}
}
temp_file.sync_all().map_err(|e| {
let _ = std::fs::remove_file(&temp_path);
AppError::from(e)
})?;
if total_written != content_length {
let _ = std::fs::remove_file(&temp_path);
return Err(AppError::BadRequest);
}
debug!(
"Successfully streamed request body to disk: {} bytes at {temp_path:?}",
total_written
);
Ok((temp_path, total_written as u64))
}
fn decode_url(path: &str) -> Result<String, AppError> {
let mut decoded = String::with_capacity(path.len());
let mut chars = path.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
let hex1 = chars.next().ok_or(AppError::BadRequest)?;
let hex2 = chars.next().ok_or(AppError::BadRequest)?;
if let Ok(byte_val) = u8::from_str_radix(&format!("{hex1}{hex2}"), 16) {
if let Some(decoded_char) = char::from_u32(byte_val as u32) {
decoded.push(decoded_char);
} else {
decoded.push(ch);
decoded.push(hex1);
decoded.push(hex2);
}
} else {
decoded.push(ch);
decoded.push(hex1);
decoded.push(hex2);
}
} else {
decoded.push(ch);
}
}
Ok(decoded)
}
pub fn cleanup(&self) {
if let Some(RequestBody::File { path, .. }) = &self.body {
if let Err(e) = std::fs::remove_file(path) {
warn!("Failed to clean up temporary file {path:?}: {e}");
} else {
debug!("Cleaned up temporary file: {path:?}");
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn handle_client(
mut stream: ClientStream,
base_dir: &Arc<PathBuf>,
allowed_extensions: &Arc<Vec<glob::Pattern>>,
username: &Arc<Option<String>>,
password: &Arc<Option<String>>,
chunk_size: usize,
cli_config: Option<&crate::cli::Cli>,
stats: Option<&crate::server::ServerStats>,
router: &Arc<Router>,
) {
let log_prefix = format!("[{}]", stream.peer_addr().unwrap());
debug!("{} Handling client connection", log_prefix);
trace!(
"{} Client connection established, starting request processing",
log_prefix
);
let request = match Request::from_stream(&mut stream) {
Ok(req) => {
debug!(
"{} Successfully parsed request: {} {}",
log_prefix, req.method, req.path
);
trace!(
"{} Request headers count: {}",
log_prefix,
req.headers.len()
);
req
}
Err(e) => {
warn!("{log_prefix} Failed to parse request: {e}");
debug!("{} Sending error response for parse failure", log_prefix);
send_error_response(&mut stream, e, &log_prefix);
return;
}
};
let start_time = std::time::Instant::now();
let response_result = route_request(
&request,
base_dir,
allowed_extensions,
username,
password,
chunk_size,
cli_config,
stats,
router,
);
let processing_time = start_time.elapsed();
debug!("{} Request processed in {:?}", log_prefix, processing_time);
match response_result {
Ok(response) => {
trace!("{} Response status: {}", log_prefix, response.status_code);
match send_response(&mut stream, response, &log_prefix) {
Ok(body_bytes) => {
trace!(
"{} Response sent successfully, {} bytes",
log_prefix, body_bytes
);
if let Some(stats) = stats {
stats.record_request(true, body_bytes);
}
}
Err(e) => {
error!("{log_prefix} Failed to send response: {e}");
if let Some(stats) = stats {
stats.record_request(false, 0);
}
}
}
}
Err(e) => {
warn!("{log_prefix} Error processing request: {e}");
debug!(
"{} Sending error response for processing failure",
log_prefix
);
send_error_response(&mut stream, e, &log_prefix);
if let Some(stats) = stats {
stats.record_request(false, 0);
}
}
}
request.cleanup();
}
#[allow(clippy::too_many_arguments)]
fn route_request(
request: &Request,
base_dir: &Arc<PathBuf>,
allowed_extensions: &Arc<Vec<glob::Pattern>>,
_username: &Arc<Option<String>>,
_password: &Arc<Option<String>>,
chunk_size: usize,
cli_config: Option<&crate::cli::Cli>,
_stats: Option<&crate::server::ServerStats>,
router: &Arc<Router>,
) -> Result<Response, AppError> {
trace!("Routing {} {} through router", request.method, request.path);
if let Some(router_response) = router.route(request) {
debug!(
"Route found in router for {} {}",
request.method, request.path
);
trace!("Router handler execution starting");
return router_response;
}
if request.path.starts_with("/_irondrop/") {
debug!("Internal path {} not found in router", request.path);
return Err(AppError::NotFound);
}
debug!("Handling file request for path: {}", request.path);
trace!("Using file handler for non-internal path");
crate::handlers::handle_file_request(
request,
base_dir,
allowed_extensions,
chunk_size,
cli_config,
)
}
fn send_response(
stream: &mut ClientStream,
response: Response,
log_prefix: &str,
) -> Result<u64, std::io::Error> {
info!(
"{} {} {}",
log_prefix, response.status_code, response.status_text
);
debug!(
"{} Preparing response headers ({} custom headers)",
log_prefix,
response.headers.len()
);
let mut response_str = format!(
"HTTP/1.1 {} {}
",
response.status_code, response.status_text
);
response_str.push_str(&format!(
"Server: irondrop/{}
",
crate::VERSION
));
response_str.push_str(
"Connection: close
",
);
for (key, value) in &response.headers {
trace!("{} Response header: {}: {}", log_prefix, key, value);
response_str.push_str(&format!(
"{key}: {value}
"
));
}
let has_content_length = response
.headers
.keys()
.any(|k| k.to_lowercase() == "content-length");
if !has_content_length {
let length = match &response.body {
ResponseBody::Text(text) => text.len(),
ResponseBody::StaticText(text) => text.len(),
ResponseBody::Binary(bytes) => bytes.len(),
ResponseBody::StaticBinary(bytes) => bytes.len(),
ResponseBody::Stream(file_details) => file_details.size as usize,
};
response_str.push_str(&format!(
"Content-Length: {length}
"
));
}
response_str.push_str("\r\n");
debug!(
"{} Sending response headers ({} bytes)",
log_prefix,
response_str.len()
);
stream.write_all(response_str.as_bytes())?;
let mut body_sent: u64 = 0;
debug!("{} Starting body transmission", log_prefix);
match response.body {
ResponseBody::Text(text) => {
let bytes = text.as_bytes();
trace!("{} Sending {} bytes of text data", log_prefix, bytes.len());
stream.write_all(bytes)?;
body_sent += bytes.len() as u64;
}
ResponseBody::StaticText(text) => {
let bytes = text.as_bytes();
trace!(
"{} Sending {} bytes of static text",
log_prefix,
bytes.len()
);
stream.write_all(bytes)?;
body_sent += bytes.len() as u64;
}
ResponseBody::Binary(bytes) => {
trace!(
"{} Sending {} bytes of binary data",
log_prefix,
bytes.len()
);
stream.write_all(&bytes)?;
body_sent += bytes.len() as u64;
}
ResponseBody::StaticBinary(bytes) => {
trace!(
"{} Sending {} bytes of static binary data",
log_prefix,
bytes.len()
);
stream.write_all(bytes)?;
body_sent += bytes.len() as u64;
}
ResponseBody::Stream(mut file_details) => {
trace!(
"{} Streaming file: {} bytes, chunk size: {}",
log_prefix, file_details.size, file_details.chunk_size
);
let mut buffer = vec![0; file_details.chunk_size];
let mut chunks_sent = 0;
loop {
let bytes_read = file_details.file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
stream.write_all(&buffer[..bytes_read])?;
body_sent += bytes_read as u64;
chunks_sent += 1;
if chunks_sent % 100 == 0 {
trace!(
"{} Streamed {} chunks ({} bytes so far)",
log_prefix, chunks_sent, body_sent
);
}
}
debug!(
"{} File streaming completed: {} chunks, {} bytes total",
log_prefix, chunks_sent, body_sent
);
}
}
stream.flush()?;
Ok(body_sent)
}
fn send_error_response(stream: &mut ClientStream, error: AppError, log_prefix: &str) {
let (status_code, status_text) = match error {
AppError::NotFound => (404, "Not Found"),
AppError::Forbidden => (403, "Forbidden"),
AppError::BadRequest => (400, "Bad Request"),
AppError::Unauthorized => (401, "Unauthorized"),
AppError::MethodNotAllowed => (405, "Method Not Allowed"),
AppError::PayloadTooLarge(_) => (413, "Payload Too Large"),
AppError::InvalidFilename(_) => (400, "Bad Request"),
AppError::UploadDiskFull(_) => (507, "Insufficient Storage"),
AppError::UnsupportedMediaType(_) => (415, "Unsupported Media Type"),
AppError::UploadDisabled => (403, "Forbidden"),
_ => (500, "Internal Server Error"),
};
info!("{log_prefix} {status_code} {status_text}");
let response = create_error_response(status_code, status_text);
if let Err(e) = response.send(stream, log_prefix) {
error!("{log_prefix} Failed to send error response: {e}");
}
}