@@ -6,6 +6,7 @@ use crate::response::create_error_response;
66use crate :: router:: Router ;
77use log:: { debug, error, info, warn} ;
88use std:: collections:: HashMap ;
9+ use std:: fs:: File ;
910use std:: io:: prelude:: * ;
1011use std:: net:: TcpStream ;
1112use std:: path:: PathBuf ;
@@ -17,13 +18,40 @@ const MAX_REQUEST_BODY_SIZE: usize = 10 * 1024 * 1024 * 1024;
1718/// Maximum size for request headers (8KB) to prevent header buffer overflow
1819const MAX_HEADERS_SIZE : usize = 8 * 1024 ;
1920
21+ /// Threshold for streaming request bodies to disk (128MB)
22+ pub const STREAM_TO_DISK_THRESHOLD : usize = 128 * 1024 * 1024 ;
23+
2024/// Represents a parsed incoming HTTP request.
2125#[ derive( Debug ) ]
2226pub struct Request {
2327 pub method : String ,
2428 pub path : String ,
2529 pub headers : HashMap < String , String > ,
26- pub body : Option < Vec < u8 > > ,
30+ pub body : Option < RequestBody > ,
31+ }
32+
33+ /// Request body can be either in memory or streamed to disk for large uploads
34+ #[ derive( Debug ) ]
35+ pub enum RequestBody {
36+ /// Small bodies stored in memory
37+ Memory ( Vec < u8 > ) ,
38+ /// Large bodies streamed to temporary file
39+ File { path : PathBuf , size : u64 } ,
40+ }
41+
42+ impl RequestBody {
43+ /// Get the size of the request body in bytes
44+ pub fn len ( & self ) -> usize {
45+ match self {
46+ RequestBody :: Memory ( data) => data. len ( ) ,
47+ RequestBody :: File { size, .. } => * size as usize ,
48+ }
49+ }
50+
51+ /// Check if the request body is empty
52+ pub fn is_empty ( & self ) -> bool {
53+ self . len ( ) == 0
54+ }
2755}
2856
2957/// Represents an outgoing HTTP response.
@@ -182,11 +210,12 @@ impl Request {
182210 }
183211
184212 /// Read request body based on Content-Length header with security validations
213+ /// Large bodies are streamed to disk to prevent memory exhaustion
185214 fn read_request_body (
186215 stream : & mut TcpStream ,
187216 headers : & HashMap < String , String > ,
188217 remaining_bytes : Vec < u8 > ,
189- ) -> Result < Option < Vec < u8 > > , AppError > {
218+ ) -> Result < Option < RequestBody > , AppError > {
190219 // Check if we have a Content-Length header
191220 let content_length = match headers. get ( "content-length" ) {
192221 Some ( length_str) => match length_str. parse :: < usize > ( ) {
@@ -208,13 +237,31 @@ impl Request {
208237
209238 // Validate content length against security limits
210239 if content_length == 0 {
211- return Ok ( Some ( Vec :: new ( ) ) ) ;
240+ return Ok ( Some ( RequestBody :: Memory ( Vec :: new ( ) ) ) ) ;
212241 }
213242
214243 if content_length > MAX_REQUEST_BODY_SIZE {
215244 return Err ( AppError :: PayloadTooLarge ( MAX_REQUEST_BODY_SIZE as u64 ) ) ;
216245 }
217246
247+ // Decide whether to use memory or disk based on size
248+ if content_length <= STREAM_TO_DISK_THRESHOLD {
249+ // Small body - use memory
250+ Self :: read_body_to_memory ( stream, content_length, remaining_bytes)
251+ . map ( |body| Some ( RequestBody :: Memory ( body) ) )
252+ } else {
253+ // Large body - stream to disk
254+ Self :: read_body_to_disk ( stream, content_length, remaining_bytes)
255+ . map ( |( path, size) | Some ( RequestBody :: File { path, size } ) )
256+ }
257+ }
258+
259+ /// Read small request body into memory
260+ fn read_body_to_memory (
261+ stream : & mut TcpStream ,
262+ content_length : usize ,
263+ remaining_bytes : Vec < u8 > ,
264+ ) -> Result < Vec < u8 > , AppError > {
218265 let mut body = Vec :: with_capacity ( content_length) ;
219266
220267 // Use any remaining bytes from header parsing
@@ -225,7 +272,7 @@ impl Request {
225272 let bytes_needed = content_length - bytes_from_headers;
226273
227274 if bytes_needed > 0 {
228- // Read the remaining body in chunks to avoid large allocations
275+ // Read the remaining body in chunks
229276 let mut bytes_read = 0 ;
230277 let chunk_size = 8192 ; // 8KB chunks
231278 let mut buffer = vec ! [ 0 ; chunk_size] ;
@@ -235,7 +282,6 @@ impl Request {
235282
236283 match stream. read ( & mut buffer[ ..to_read] ) {
237284 Ok ( 0 ) => {
238- // Unexpected end of stream
239285 return Err ( AppError :: BadRequest ) ;
240286 }
241287 Ok ( n) => {
@@ -257,8 +303,103 @@ impl Request {
257303 return Err ( AppError :: BadRequest ) ;
258304 }
259305
260- debug ! ( "Successfully read request body: {} bytes" , body. len( ) ) ;
261- Ok ( Some ( body) )
306+ debug ! (
307+ "Successfully read request body to memory: {} bytes" ,
308+ body. len( )
309+ ) ;
310+ Ok ( body)
311+ }
312+
313+ /// Read large request body directly to disk to prevent memory exhaustion
314+ fn read_body_to_disk (
315+ stream : & mut TcpStream ,
316+ content_length : usize ,
317+ remaining_bytes : Vec < u8 > ,
318+ ) -> Result < ( PathBuf , u64 ) , AppError > {
319+ // Create temporary file for the request body
320+ let temp_filename = format ! (
321+ "irondrop_request_{}_{:x}.tmp" ,
322+ std:: process:: id( ) ,
323+ std:: time:: SystemTime :: now( )
324+ . duration_since( std:: time:: UNIX_EPOCH )
325+ . unwrap_or_default( )
326+ . as_nanos( )
327+ ) ;
328+
329+ // Use system temp directory
330+ let temp_dir = std:: env:: temp_dir ( ) ;
331+ let temp_path = temp_dir. join ( & temp_filename) ;
332+
333+ let mut temp_file = File :: create ( & temp_path) . map_err ( |e| {
334+ error ! ( "Failed to create temporary file {temp_path:?}: {e}" ) ;
335+ AppError :: from ( e)
336+ } ) ?;
337+
338+ let mut total_written = 0 ;
339+
340+ // Write any remaining bytes from header parsing
341+ if !remaining_bytes. is_empty ( ) {
342+ let bytes_to_write = remaining_bytes. len ( ) . min ( content_length) ;
343+ temp_file
344+ . write_all ( & remaining_bytes[ ..bytes_to_write] )
345+ . map_err ( |e| {
346+ let _ = std:: fs:: remove_file ( & temp_path) ;
347+ AppError :: from ( e)
348+ } ) ?;
349+ total_written += bytes_to_write;
350+ }
351+
352+ // Stream remaining bytes directly to disk
353+ let bytes_needed = content_length - total_written;
354+ if bytes_needed > 0 {
355+ let mut bytes_read = 0 ;
356+ let chunk_size = 64 * 1024 ; // 64KB chunks for better disk I/O
357+ let mut buffer = vec ! [ 0 ; chunk_size] ;
358+
359+ while bytes_read < bytes_needed {
360+ let to_read = ( bytes_needed - bytes_read) . min ( chunk_size) ;
361+
362+ match stream. read ( & mut buffer[ ..to_read] ) {
363+ Ok ( 0 ) => {
364+ let _ = std:: fs:: remove_file ( & temp_path) ;
365+ return Err ( AppError :: BadRequest ) ;
366+ }
367+ Ok ( n) => {
368+ temp_file. write_all ( & buffer[ ..n] ) . map_err ( |e| {
369+ let _ = std:: fs:: remove_file ( & temp_path) ;
370+ AppError :: from ( e)
371+ } ) ?;
372+ bytes_read += n;
373+ total_written += n;
374+ }
375+ Err ( e) => {
376+ let _ = std:: fs:: remove_file ( & temp_path) ;
377+ if e. kind ( ) == std:: io:: ErrorKind :: TimedOut {
378+ warn ! ( "Request body read timeout" ) ;
379+ }
380+ return Err ( AppError :: Io ( e) ) ;
381+ }
382+ }
383+ }
384+ }
385+
386+ // Ensure all data is written to disk
387+ temp_file. sync_all ( ) . map_err ( |e| {
388+ let _ = std:: fs:: remove_file ( & temp_path) ;
389+ AppError :: from ( e)
390+ } ) ?;
391+
392+ // Verify we read exactly the expected amount
393+ if total_written != content_length {
394+ let _ = std:: fs:: remove_file ( & temp_path) ;
395+ return Err ( AppError :: BadRequest ) ;
396+ }
397+
398+ debug ! (
399+ "Successfully streamed request body to disk: {} bytes at {temp_path:?}" ,
400+ total_written
401+ ) ;
402+ Ok ( ( temp_path, total_written as u64 ) )
262403 }
263404
264405 /// Simple URL decoding for percent-encoded paths
@@ -294,6 +435,17 @@ impl Request {
294435
295436 Ok ( decoded)
296437 }
438+
439+ /// Clean up any temporary files associated with this request
440+ pub fn cleanup ( & self ) {
441+ if let Some ( RequestBody :: File { path, .. } ) = & self . body {
442+ if let Err ( e) = std:: fs:: remove_file ( path) {
443+ warn ! ( "Failed to clean up temporary file {path:?}: {e}" ) ;
444+ } else {
445+ debug ! ( "Cleaned up temporary file: {path:?}" ) ;
446+ }
447+ }
448+ }
297449}
298450
299451/// Top-level function to handle a client connection.
@@ -354,6 +506,9 @@ pub fn handle_client(
354506 }
355507 }
356508 }
509+
510+ // Clean up any temporary files created during request processing
511+ request. cleanup ( ) ;
357512}
358513
359514// Static asset, favicon, upload, and health handlers moved to handlers.rs
0 commit comments