diff --git a/src/server.rs b/src/server.rs index 5bb195a..da9da9a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -843,7 +843,7 @@ impl Server { } } - let range = if use_range { + let ranges = if use_range { headers.get(RANGE).map(|range| { range .to_str() @@ -864,27 +864,59 @@ impl Server { res.headers_mut().typed_insert(AcceptRanges::bytes()); - if let Some(range) = range { - if let Some((start, end)) = range { - file.seek(SeekFrom::Start(start)).await?; - let range_size = end - start + 1; - *res.status_mut() = StatusCode::PARTIAL_CONTENT; - let content_range = format!("bytes {}-{}/{}", start, end, size); - res.headers_mut() - .insert(CONTENT_RANGE, content_range.parse()?); - res.headers_mut() - .insert(CONTENT_LENGTH, format!("{range_size}").parse()?); - if head_only { - return Ok(()); - } + if let Some(ranges) = ranges { + if let Some(ranges) = ranges { + if ranges.len() == 1 { + let (start, end) = ranges[0]; + file.seek(SeekFrom::Start(start)).await?; + let range_size = end - start + 1; + *res.status_mut() = StatusCode::PARTIAL_CONTENT; + let content_range = format!("bytes {}-{}/{}", start, end, size); + res.headers_mut() + .insert(CONTENT_RANGE, content_range.parse()?); + res.headers_mut() + .insert(CONTENT_LENGTH, format!("{range_size}").parse()?); + if head_only { + return Ok(()); + } - let stream_body = StreamBody::new( - LengthLimitedStream::new(file, range_size as usize) - .map_ok(Frame::data) - .map_err(|err| anyhow!("{err}")), - ); - let boxed_body = stream_body.boxed(); - *res.body_mut() = boxed_body; + let stream_body = StreamBody::new( + LengthLimitedStream::new(file, range_size as usize) + .map_ok(Frame::data) + .map_err(|err| anyhow!("{err}")), + ); + let boxed_body = stream_body.boxed(); + *res.body_mut() = boxed_body; + } else { + *res.status_mut() = StatusCode::PARTIAL_CONTENT; + let boundary = Uuid::new_v4(); + let mut body = Vec::new(); + let content_type = get_content_type(path).await?; + for (start, end) in ranges { + file.seek(SeekFrom::Start(start)).await?; + let range_size = end - start + 1; + let content_range = format!("bytes {}-{}/{}", start, end, size); + let part_header = format!( + "--{boundary}\r\nContent-Type: {content_type}\r\nContent-Range: {content_range}\r\n\r\n", + ); + body.extend_from_slice(part_header.as_bytes()); + let mut buffer = vec![0; range_size as usize]; + file.read_exact(&mut buffer).await?; + body.extend_from_slice(&buffer); + body.extend_from_slice(b"\r\n"); + } + body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes()); + res.headers_mut().insert( + CONTENT_TYPE, + format!("multipart/byteranges; boundary={boundary}").parse()?, + ); + res.headers_mut() + .insert(CONTENT_LENGTH, format!("{}", body.len()).parse()?); + if head_only { + return Ok(()); + } + *res.body_mut() = body_full(body); + } } else { *res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; res.headers_mut() @@ -1771,8 +1803,10 @@ fn parse_upload_offset(headers: &HeaderMap, size: u64) -> Result Result { diff --git a/src/utils.rs b/src/utils.rs index edf8544..600f1b3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -100,36 +100,42 @@ pub fn load_private_key>(filename: T) -> Result Option<(u64, u64)> { - let (unit, range) = range.split_once('=')?; - if unit != "bytes" || range.contains(',') { +pub fn parse_range(range: &str, size: u64) -> Option> { + let (unit, ranges) = range.split_once('=')?; + if unit != "bytes" { return None; } - let (start, end) = range.split_once('-')?; - if start.is_empty() { - let offset = end.parse::().ok()?; - if offset <= size { - Some((size - offset, size - 1)) - } else { - None - } - } else { - let start = start.parse::().ok()?; - if start < size { - if end.is_empty() { - Some((start, size - 1)) + + let mut result = Vec::new(); + for range in ranges.split(',') { + let (start, end) = range.trim().split_once('-')?; + if start.is_empty() { + let offset = end.parse::().ok()?; + if offset <= size { + result.push((size - offset, size - 1)); } else { - let end = end.parse::().ok()?; - if end < size { - Some((start, end)) - } else { - None - } + return None; } } else { - None + let start = start.parse::().ok()?; + if start < size { + if end.is_empty() { + result.push((start, size - 1)); + } else { + let end = end.parse::().ok()?; + if end < size { + result.push((start, end)); + } else { + return None; + } + } + } else { + return None; + } } } + + Some(result) } #[cfg(test)] @@ -162,13 +168,19 @@ mod tests { #[test] fn test_parse_range() { - assert_eq!(parse_range("bytes=0-499", 500), Some((0, 499))); - assert_eq!(parse_range("bytes=0-", 500), Some((0, 499))); - assert_eq!(parse_range("bytes=299-", 500), Some((299, 499))); - assert_eq!(parse_range("bytes=-500", 500), Some((0, 499))); - assert_eq!(parse_range("bytes=-300", 500), Some((200, 499))); + assert_eq!(parse_range("bytes=0-499", 500), Some(vec![(0, 499)])); + assert_eq!(parse_range("bytes=0-", 500), Some(vec![(0, 499)])); + assert_eq!(parse_range("bytes=299-", 500), Some(vec![(299, 499)])); + assert_eq!(parse_range("bytes=-500", 500), Some(vec![(0, 499)])); + assert_eq!(parse_range("bytes=-300", 500), Some(vec![(200, 499)])); + assert_eq!( + parse_range("bytes=0-199, 100-399, 400-, -200", 500), + Some(vec![(0, 199), (100, 399), (400, 499), (300, 499)]) + ); assert_eq!(parse_range("bytes=500-", 500), None); assert_eq!(parse_range("bytes=-501", 500), None); assert_eq!(parse_range("bytes=0-500", 500), None); + assert_eq!(parse_range("bytes=0-199,", 500), None); + assert_eq!(parse_range("bytes=0-199, 500-", 500), None); } } diff --git a/tests/range.rs b/tests/range.rs index 511c244..cb56889 100644 --- a/tests/range.rs +++ b/tests/range.rs @@ -2,7 +2,7 @@ mod fixtures; mod utils; use fixtures::{server, Error, TestServer}; -use reqwest::header::HeaderValue; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use rstest::rstest; #[rstest] @@ -39,3 +39,68 @@ fn get_file_range_invalid(server: TestServer) -> Result<(), Error> { assert_eq!(resp.headers().get("content-range").unwrap(), "bytes */18"); Ok(()) } + +fn parse_multipart_body<'a>(body: &'a str, boundary: &str) -> Vec<(HeaderMap, &'a str)> { + body.split(&format!("--{}", boundary)) + .filter(|part| !part.is_empty() && *part != "--\r\n") + .map(|part| { + let (head, body) = part.trim_ascii().split_once("\r\n\r\n").unwrap(); + let headers = head + .split("\r\n") + .fold(HeaderMap::new(), |mut headers, header| { + let (key, value) = header.split_once(":").unwrap(); + let key = HeaderName::from_bytes(key.as_bytes()).unwrap(); + let value = HeaderValue::from_str(value.trim_ascii_start()).unwrap(); + headers.insert(key, value); + headers + }); + (headers, body) + }) + .collect() +} + +#[rstest] +fn get_file_multipart_range(server: TestServer) -> Result<(), Error> { + let resp = fetch!(b"GET", format!("{}index.html", server.url())) + .header("range", HeaderValue::from_static("bytes=0-11, 6-17")) + .send()?; + assert_eq!(resp.status(), 206); + assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes"); + + let content_type = resp + .headers() + .get("content-type") + .unwrap() + .to_str()? + .to_string(); + assert!(content_type.starts_with("multipart/byteranges; boundary=")); + + let boundary = content_type.split_once('=').unwrap().1.trim_ascii_start(); + assert!(!boundary.is_empty()); + + let body = resp.text()?; + let parts = parse_multipart_body(&body, boundary); + assert_eq!(parts.len(), 2); + + let (headers, body) = &parts[0]; + assert_eq!(headers.get("content-range").unwrap(), "bytes 0-11/18"); + assert_eq!(*body, "This is inde"); + + let (headers, body) = &parts[1]; + assert_eq!(headers.get("content-range").unwrap(), "bytes 6-17/18"); + assert_eq!(*body, "s index.html"); + + Ok(()) +} + +#[rstest] +fn get_file_multipart_range_invalid(server: TestServer) -> Result<(), Error> { + let resp = fetch!(b"GET", format!("{}index.html", server.url())) + .header("range", HeaderValue::from_static("bytes=0-6, 20-30")) + .send()?; + assert_eq!(resp.status(), 416); + assert_eq!(resp.headers().get("content-range").unwrap(), "bytes */18"); + assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes"); + assert_eq!(resp.headers().get("content-length").unwrap(), "0"); + Ok(()) +}