1use http_body_util::{combinators::UnsyncBoxBody, BodyExt, Full};
26use hyper::body::{Bytes, Frame, Incoming};
27use hyper::Request;
28use rust_webx_core::auth::IClaims;
29use rust_webx_core::config::AppOptions;
30use rust_webx_core::error::{Error, Result};
31use rust_webx_core::form::{FormFileBuilder, MultipartForm};
32use rust_webx_core::http::{
33 content_disposition_value, ByteRange, FileBody, FileSource, IClaimsExt, IHttpContext,
34 IHttpRequest, IHttpResponse, ResponseBody, SeekableReader,
35};
36use futures_util::{StreamExt, TryStreamExt};
37use std::collections::HashMap;
38use std::path::{Path, PathBuf};
39use std::sync::Arc;
40use std::time::{SystemTime, UNIX_EPOCH};
41use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, SeekFrom};
42use tokio::sync::Mutex;
43
44use crate::problem_response::{build_problem, problem_to_bytes};
45
46pub type RespBody = UnsyncBoxBody<Bytes, std::io::Error>;
52
53#[derive(Debug, Clone)]
57pub struct BodyLimits {
58 pub body: usize,
60 pub multipart_body: u64,
62 pub file: u64,
64 pub field: usize,
66 pub memory_threshold: usize,
68 pub spool_dir: PathBuf,
70}
71
72impl Default for BodyLimits {
73 fn default() -> Self {
74 Self {
75 body: 10 * 1024 * 1024,
76 multipart_body: 256 * 1024 * 1024,
77 file: 128 * 1024 * 1024,
78 field: 1024 * 1024,
79 memory_threshold: 1024 * 1024,
80 spool_dir: rust_webx_core::form::spool_root(),
81 }
82 }
83}
84
85impl BodyLimits {
86 pub fn from_options(options: &AppOptions) -> Self {
91 let spool_dir = match options.form.temp_dir.as_deref() {
92 Some(dir) if !dir.trim().is_empty() => {
93 let path = PathBuf::from(dir);
94 if path.is_absolute() {
95 path
96 } else {
97 rust_webx_core::paths::app_base().join(path)
98 }
99 }
100 _ => rust_webx_core::form::spool_root(),
101 };
102
103 Self {
104 body: options.app.max_body_size,
105 multipart_body: options.form.max_request_size as u64,
106 file: options.form.max_file_size,
107 field: options.form.max_field_size,
108 memory_threshold: options.form.memory_threshold,
109 spool_dir,
110 }
111 }
112
113 pub fn apply_spool_root(&self) {
116 rust_webx_core::form::set_spool_root(self.spool_dir.clone());
117 }
118}
119
120pub struct HttpContext {
122 req: HttpRequest,
123 resp: HttpResponse,
124 claims: Option<Box<dyn IClaims>>,
126}
127
128impl HttpContext {
129 pub fn new(req: Request<Incoming>, limits: BodyLimits) -> Self {
134 let (parts, body) = req.into_parts();
135
136 let mut headers = HashMap::new();
137 for (name, value) in parts.headers.iter() {
138 if let Ok(value) = value.to_str() {
139 headers.insert(name.to_string(), value.to_string());
140 }
141 }
142
143 let query_params = parts
144 .uri
145 .query()
146 .map(parse_query_string)
147 .unwrap_or_default();
148
149 let mut resp = HttpResponse::new(200);
150
151 let declared_limit = if is_multipart_content_type(headers.get("content-type")) {
155 limits.multipart_body
156 } else {
157 limits.body as u64
158 };
159 if let Some(declared) = headers
160 .get("content-length")
161 .and_then(|value| value.parse::<u64>().ok())
162 {
163 if declared > declared_limit {
164 let problem = build_problem(
165 413,
166 format!("request body exceeds the {declared_limit}-byte limit"),
167 );
168 resp.set_status(413);
169 resp.set_header("content-type", "application/problem+json");
170 resp.body = Some(ResponseBody::Bytes(problem_to_bytes(&problem)));
171 }
172 }
173
174 let req = HttpRequest {
175 method: parts.method.to_string(),
176 path: parts.uri.path().to_string(),
177 headers,
178 query_params,
179 route_params: HashMap::new(),
180 route_pattern: None,
181 body: Mutex::new(RequestBody::Pending(Some(body))),
182 limits,
183 };
184
185 Self { req, resp, claims: None }
186 }
187
188 pub async fn into_response(self) -> hyper::Response<RespBody> {
190 let HttpContext { req, resp, .. } = self;
191 finalize_response(&req, resp).await
192 }
193}
194
195impl IClaimsExt for HttpContext {
196 fn set_claims(&mut self, claims: Box<dyn IClaims>) {
197 self.claims = Some(claims);
198 }
199
200 fn claims(&self) -> Option<&dyn IClaims> {
201 self.claims.as_deref()
202 }
203}
204
205impl IHttpContext for HttpContext {
206 fn request(&self) -> &dyn IHttpRequest {
207 &self.req
208 }
209
210 fn request_mut(&mut self) -> &mut dyn IHttpRequest {
211 &mut self.req
212 }
213
214 fn response(&self) -> &dyn IHttpResponse {
215 &self.resp
216 }
217
218 fn response_mut(&mut self) -> &mut dyn IHttpResponse {
219 &mut self.resp
220 }
221}
222
223enum RequestBody {
229 Pending(Option<Incoming>),
231 Buffered(Vec<u8>),
233 Form(Arc<MultipartForm>),
235}
236
237pub struct HttpRequest {
239 method: String,
240 path: String,
241 headers: HashMap<String, String>,
242 query_params: HashMap<String, String>,
243 route_params: HashMap<String, String>,
244 route_pattern: Option<String>,
245 body: Mutex<RequestBody>,
246 limits: BodyLimits,
247}
248
249#[async_trait::async_trait]
250impl IHttpRequest for HttpRequest {
251 fn method(&self) -> &str {
252 &self.method
253 }
254
255 fn path(&self) -> &str {
256 &self.path
257 }
258
259 fn header(&self, name: &str) -> Option<&str> {
260 self.headers.get(name).map(|s| s.as_str())
261 }
262
263 fn query(&self) -> &HashMap<String, String> {
264 &self.query_params
265 }
266
267 fn route_params(&self) -> &HashMap<String, String> {
268 &self.route_params
269 }
270
271 fn route_params_mut(&mut self) -> &mut HashMap<String, String> {
272 &mut self.route_params
273 }
274
275 fn route_pattern(&self) -> Option<&str> {
276 self.route_pattern.as_deref()
277 }
278
279 fn route_pattern_mut(&mut self) -> &mut Option<String> {
280 &mut self.route_pattern
281 }
282
283 async fn body_bytes(&mut self) -> Result<Vec<u8>> {
284 let mut guard = self.body.lock().await;
285 let incoming = match &mut *guard {
286 RequestBody::Buffered(bytes) => return Ok(bytes.clone()),
287 RequestBody::Form(_) => {
288 return Err(Error::Http(
289 "request body was already read as multipart/form-data".to_string(),
290 ))
291 }
292 RequestBody::Pending(slot) => slot.take(),
293 };
294
295 let Some(incoming) = incoming else {
296 return Err(Error::Http(
297 "request body has already been consumed".to_string(),
298 ));
299 };
300
301 let bytes = read_body(incoming, self.limits.body).await?;
302 *guard = RequestBody::Buffered(bytes.clone());
303 Ok(bytes)
304 }
305
306 async fn body_text(&mut self) -> Result<String> {
307 let bytes = self.body_bytes().await?;
308 String::from_utf8(bytes).map_err(|e| Error::Http(e.to_string()))
309 }
310
311 async fn multipart(&mut self) -> Result<Arc<MultipartForm>> {
312 let mut guard = self.body.lock().await;
313
314 match &*guard {
315 RequestBody::Form(form) => return Ok(Arc::clone(form)),
316 RequestBody::Buffered(_) => {
317 return Err(Error::UnsupportedMediaType(
318 "request body was already read as a non-multipart body".to_string(),
319 ))
320 }
321 RequestBody::Pending(_) => {}
322 }
323
324 if !self.is_multipart() {
325 return Err(Error::UnsupportedMediaType(
326 "expected Content-Type: multipart/form-data".to_string(),
327 ));
328 }
329
330 let raw_content_type = self.header("content-type").unwrap_or("");
331 let boundary = multipart_boundary(raw_content_type).ok_or_else(|| {
332 Error::Validation(
333 "multipart/form-data request is missing its boundary parameter".to_string(),
334 )
335 })?;
336
337 let incoming = match &mut *guard {
338 RequestBody::Pending(slot) => slot.take(),
339 _ => None,
340 };
341
342 let Some(body) = incoming else {
343 return Err(Error::Http(
344 "request body has already been consumed".to_string(),
345 ));
346 };
347
348 let form = Arc::new(parse_multipart(body, &boundary, &self.limits).await?);
349 *guard = RequestBody::Form(Arc::clone(&form));
350 Ok(form)
351 }
352}
353
354async fn read_body(incoming: Incoming, limit: usize) -> Result<Vec<u8>> {
356 let mut out = Vec::new();
357 let mut total = 0usize;
358 let mut stream = incoming.into_data_stream();
359
360 while let Some(chunk) = stream.next().await {
361 let chunk = chunk.map_err(|err| Error::Http(format!("failed to read request body: {err}")))?;
362 total = total.saturating_add(chunk.len());
363 if total > limit {
364 return Err(Error::PayloadTooLarge(format!(
365 "request body exceeds the {limit}-byte limit"
366 )));
367 }
368 out.extend_from_slice(&chunk);
369 }
370
371 Ok(out)
372}
373
374fn is_multipart_content_type(content_type: Option<&String>) -> bool {
376 content_type
377 .map(|value| {
378 value
379 .split(';')
380 .next()
381 .unwrap_or("")
382 .trim()
383 .eq_ignore_ascii_case("multipart/form-data")
384 })
385 .unwrap_or(false)
386}
387
388fn multipart_boundary(content_type: &str) -> Option<String> {
390 for part in content_type.split(';').skip(1) {
391 let part = part.trim();
392 let Some((key, value)) = part.split_once('=') else {
393 continue;
394 };
395 if key.trim().eq_ignore_ascii_case("boundary") {
396 let value = value.trim().trim_matches('"');
397 if !value.is_empty() {
398 return Some(value.to_string());
399 }
400 }
401 }
402 None
403}
404
405async fn parse_multipart(
412 incoming: Incoming,
413 boundary: &str,
414 limits: &BodyLimits,
415) -> Result<MultipartForm> {
416 let stream = incoming.into_data_stream();
417 let mut multipart = multer::Multipart::new(stream, boundary);
418 let mut form = MultipartForm::new();
419 let mut total: u64 = 0;
420
421 while let Some(field) = multipart.next_field().await.map_err(multer_error)? {
422 let field_name = field.name().unwrap_or_default().to_string();
423 if field_name.is_empty() {
424 continue;
425 }
426 let file_name = field.file_name().map(str::to_string);
427 let content_type = field.content_type().map(|m| m.to_string());
428
429 match file_name {
430 Some(file_name) => {
431 let mut builder = FormFileBuilder::new(field_name.clone(), file_name, content_type)
432 .memory_threshold(limits.memory_threshold)
433 .max_size(limits.file)
434 .spool_dir(limits.spool_dir.clone());
435
436 let mut field = field;
437 while let Some(chunk) = field.chunk().await.map_err(multer_error)? {
438 total = charge(&mut total, chunk.len(), limits.multipart_body)?;
439 builder.write(&chunk).await?;
440 }
441 form.push_file(builder.finish().await?);
442 }
443 None => {
444 let mut value: Vec<u8> = Vec::new();
445 let mut field = field;
446 while let Some(chunk) = field.chunk().await.map_err(multer_error)? {
447 total = charge(&mut total, chunk.len(), limits.multipart_body)?;
448 if value.len() + chunk.len() > limits.field {
449 return Err(Error::PayloadTooLarge(format!(
450 "form field `{field_name}` exceeds the {}-byte limit",
451 limits.field
452 )));
453 }
454 value.extend_from_slice(&chunk);
455 }
456 let text = String::from_utf8(value).map_err(|_| {
457 Error::Validation(format!("form field `{field_name}` is not valid UTF-8"))
458 })?;
459 form.push_field(field_name, text);
460 }
461 }
462 }
463
464 Ok(form)
465}
466
467fn charge(total: &mut u64, len: usize, limit: u64) -> Result<u64> {
469 *total = total.saturating_add(len as u64);
470 if *total > limit {
471 return Err(Error::PayloadTooLarge(format!(
472 "multipart request exceeds the {limit}-byte limit"
473 )));
474 }
475 Ok(*total)
476}
477
478fn multer_error(err: multer::Error) -> Error {
479 match err {
480 multer::Error::FieldSizeExceeded { limit, .. } => Error::PayloadTooLarge(format!(
481 "multipart field exceeds the {limit}-byte limit"
482 )),
483 multer::Error::StreamSizeExceeded { limit } => Error::PayloadTooLarge(format!(
484 "multipart request exceeds the {limit}-byte limit"
485 )),
486 multer::Error::IncompleteFieldData { .. } | multer::Error::IncompleteStream => {
487 Error::Validation("multipart body ended unexpectedly".to_string())
488 }
489 other => Error::Validation(format!("invalid multipart body: {other}")),
490 }
491}
492
493pub struct HttpResponse {
499 status: u16,
500 headers: HashMap<String, String>,
501 body: Option<ResponseBody>,
502}
503
504impl HttpResponse {
505 pub fn new(status: u16) -> Self {
506 Self {
507 status,
508 headers: HashMap::new(),
509 body: None,
510 }
511 }
512}
513
514#[async_trait::async_trait]
515impl IHttpResponse for HttpResponse {
516 fn status(&self) -> u16 {
517 self.status
518 }
519
520 fn has_body(&self) -> bool {
521 self.body.is_some()
522 }
523
524 fn set_status(&mut self, code: u16) {
525 self.status = code;
526 }
527
528 fn set_header(&mut self, key: &str, value: &str) {
529 self.headers.insert(key.to_string(), value.to_string());
530 }
531
532 fn remove_header(&mut self, key: &str) {
533 self.headers.remove(key);
534 }
535
536 async fn write_bytes(&mut self, data: Vec<u8>) -> Result<()> {
537 self.body = Some(ResponseBody::Bytes(data));
538 Ok(())
539 }
540
541 async fn write_text(&mut self, text: &str) -> Result<()> {
542 self.body = Some(ResponseBody::Bytes(text.as_bytes().to_vec()));
543 Ok(())
544 }
545
546 async fn write_body(&mut self, body: ResponseBody) -> Result<()> {
547 self.body = Some(body);
548 Ok(())
549 }
550
551 fn body_bytes(&self) -> Vec<u8> {
552 self.body
553 .as_ref()
554 .and_then(ResponseBody::as_bytes)
555 .map(<[u8]>::to_vec)
556 .unwrap_or_default()
557 }
558
559 fn header(&self, key: &str) -> Option<&str> {
560 self.headers.get(key).map(|s| s.as_str())
561 }
562}
563
564async fn finalize_response(req: &HttpRequest, resp: HttpResponse) -> hyper::Response<RespBody> {
569 let HttpResponse {
570 status,
571 mut headers,
572 body,
573 } = resp;
574
575 match body.unwrap_or(ResponseBody::Bytes(Vec::new())) {
576 ResponseBody::Bytes(bytes) => {
577 default_cache_control(&mut headers, false);
578 bytes_response(status, headers, bytes)
579 }
580 ResponseBody::File(file) => {
581 default_cache_control(&mut headers, true);
582 file_response(req, status, headers, file).await
583 }
584 }
585}
586
587fn default_cache_control(headers: &mut HashMap<String, String>, is_file: bool) {
593 if headers.contains_key("cache-control") {
594 return;
595 }
596 headers.insert(
597 "cache-control".to_string(),
598 if is_file {
599 "public, max-age=0, must-revalidate".to_string()
600 } else {
601 "no-store".to_string()
602 },
603 );
604}
605
606enum PreparedSource {
608 Path(tokio::fs::File),
610 Seekable(Box<dyn SeekableReader>),
612 Stream(Box<dyn AsyncRead + Send + Unpin>),
614}
615
616impl PreparedSource {
617 fn is_seekable(&self) -> bool {
618 matches!(self, PreparedSource::Path(_) | PreparedSource::Seekable(_))
619 }
620}
621
622async fn file_response(
627 req: &HttpRequest,
628 status: u16,
629 mut headers: HashMap<String, String>,
630 file: FileBody,
631) -> hyper::Response<RespBody> {
632 let FileBody {
633 source,
634 content_type,
635 file_name,
636 disposition,
637 enable_range_processing,
638 last_modified,
639 entity_tag,
640 } = file;
641
642 let mut modified = last_modified;
644 let mut etag = entity_tag;
645 let mut source_path: Option<PathBuf> = None;
646 let known_len;
647
648 let prepared = match source {
649 FileSource::Path(path) => {
650 let handle = match tokio::fs::File::open(&path).await {
653 Ok(handle) => handle,
654 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
655 return problem_bytes(404, headers, "Not Found")
656 }
657 Err(err) => {
658 return problem_bytes(
659 500,
660 headers,
661 &format!("cannot open `{}`: {err}", path.display()),
662 )
663 }
664 };
665 let metadata = match handle.metadata().await {
666 Ok(metadata) => metadata,
667 Err(err) => {
668 return problem_bytes(
669 500,
670 headers,
671 &format!("cannot stat `{}`: {err}", path.display()),
672 )
673 }
674 };
675 if !metadata.is_file() {
676 return problem_bytes(404, headers, "Not Found");
677 }
678
679 let len = metadata.len();
680 if modified.is_none() {
681 modified = metadata.modified().ok();
682 }
683 if etag.is_none() {
684 etag = Some(etag_for(len, modified));
685 }
686 known_len = Some(len);
687 source_path = Some(path);
688 PreparedSource::Path(handle)
689 }
690 FileSource::SeekableStream { reader, len } => {
691 known_len = Some(len);
692 PreparedSource::Seekable(reader)
693 }
694 FileSource::Stream(reader) => {
695 known_len = None;
696 PreparedSource::Stream(reader)
697 }
698 };
699
700 let modified = modified.filter(|value| *value <= SystemTime::now());
707
708 if !headers.contains_key("content-type") {
710 let resolved = content_type
711 .or_else(|| source_path.as_deref().and_then(guess_content_type))
712 .unwrap_or_else(|| "application/octet-stream".to_string());
713 headers.insert("content-type".to_string(), resolved);
714 }
715 if !headers.contains_key("content-disposition") {
716 if let Some(name) = &file_name {
717 headers.insert(
718 "content-disposition".to_string(),
719 content_disposition_value(disposition, name),
720 );
721 }
722 }
723 if let Some(tag) = &etag {
724 headers.insert("etag".to_string(), tag.clone());
725 }
726 if let Some(modified) = modified {
727 headers.insert(
728 "last-modified".to_string(),
729 httpdate::fmt_http_date(modified),
730 );
731 }
732
733 let range_capable = enable_range_processing && prepared.is_seekable() && known_len.is_some();
736 headers.insert(
737 "accept-ranges".to_string(),
738 if range_capable { "bytes" } else { "none" }.to_string(),
739 );
740
741 if status == 200 && is_not_modified(req, etag.as_deref(), modified) {
743 headers.remove("content-length");
744 return bytes_response(304, headers, Vec::new());
745 }
746
747 let mut ranges: Vec<ByteRange> = Vec::new();
749 if status == 200 && range_capable {
750 let len = known_len.expect("range-capable sources know their length");
751 if let Some(raw) = req.header("range") {
752 if if_range_matches(req, etag.as_deref(), modified) {
756 match parse_ranges(raw, len) {
757 RangeOutcome::Ignored => {}
758 RangeOutcome::Unsatisfiable => {
759 headers.insert("content-range".to_string(), format!("bytes */{len}"));
760 return problem_bytes(416, headers, "Range Not Satisfiable");
761 }
762 RangeOutcome::Satisfiable(parsed) => ranges = parsed,
763 }
764 }
765 }
766 }
767
768 if ranges.len() > 1 && !matches!(prepared, PreparedSource::Path(_)) {
775 ranges.clear();
776 }
777 if ranges.len() > 1 {
778 return multipart_byteranges_response(prepared, headers, ranges, known_len);
779 }
780
781 let (status, start, count) = match ranges.first() {
782 Some(range) => {
783 let len = known_len.expect("range-capable sources know their length");
784 headers.insert(
785 "content-range".to_string(),
786 format!("bytes {}-{}/{len}", range.start, range.end),
787 );
788 (206, range.start, range.len())
789 }
790 None => (status, 0, known_len.unwrap_or(0)),
791 };
792
793 match prepared {
794 PreparedSource::Path(mut handle) => {
795 headers.insert("content-length".to_string(), count.to_string());
796 if count == 0 {
797 return bytes_response(status, headers, Vec::new());
798 }
799 if start > 0 {
800 if let Err(err) = handle.seek(SeekFrom::Start(start)).await {
801 return problem_bytes(500, headers, &format!("cannot seek: {err}"));
802 }
803 }
804 build_response(status, headers, reader_body(handle.take(count)))
805 }
806 PreparedSource::Seekable(mut reader) => {
807 headers.insert("content-length".to_string(), count.to_string());
808 if count == 0 {
809 return bytes_response(status, headers, Vec::new());
810 }
811 if start > 0 {
812 if let Err(err) = reader.seek(SeekFrom::Start(start)).await {
813 return problem_bytes(500, headers, &format!("cannot seek: {err}"));
814 }
815 }
816 build_response(status, headers, reader_body(reader.take(count)))
817 }
818 PreparedSource::Stream(reader) => {
819 build_response(status, headers, reader_body(reader))
822 }
823 }
824}
825
826fn representation_content_type(headers: &HashMap<String, String>) -> String {
831 headers
832 .get("content-type")
833 .filter(|value| !value.starts_with("multipart/byteranges"))
834 .cloned()
835 .unwrap_or_else(|| "application/octet-stream".to_string())
836}
837
838fn multipart_byteranges_response(
845 prepared: PreparedSource,
846 mut headers: HashMap<String, String>,
847 ranges: Vec<ByteRange>,
848 known_len: Option<u64>,
849) -> hyper::Response<RespBody> {
850 let PreparedSource::Path(handle) = prepared else {
851 unreachable!("multi-range is only routed here for path sources");
852 };
853
854 let total_len = known_len.unwrap_or(0);
855 let media_type = representation_content_type(&headers);
856 let boundary = format!("webx-{}", uuid::Uuid::new_v4().simple());
857
858 let parts: Vec<(Bytes, ByteRange)> = ranges
861 .iter()
862 .map(|range| {
863 let header = format!(
864 "--{boundary}\r\nContent-Type: {media_type}\r\nContent-Range: bytes {}-{}/{total_len}\r\n\r\n",
865 range.start, range.end
866 );
867 (Bytes::from(header), *range)
868 })
869 .collect();
870 let closing = Bytes::from(format!("--{boundary}--\r\n"));
871
872 let content_length: u64 = parts
873 .iter()
874 .map(|(header, range)| header.len() as u64 + range.len() + 2)
875 .sum::<u64>()
876 + closing.len() as u64;
877
878 headers.insert(
879 "content-type".to_string(),
880 format!("multipart/byteranges; boundary={boundary}"),
881 );
882 headers.insert("content-length".to_string(), content_length.to_string());
883 headers.remove("content-range");
885
886 let stream = multipart_stream(handle, parts, closing);
887 build_response(
888 206,
889 headers,
890 BodyExt::boxed_unsync(http_body_util::StreamBody::new(stream)),
891 )
892}
893
894const MULTIPART_CHUNK: usize = STREAM_BUF_SIZE;
896
897struct MultipartState {
899 handle: tokio::fs::File,
900 parts: std::vec::IntoIter<(Bytes, ByteRange)>,
901 closing: Bytes,
902 pending_header: Bytes,
904 phase: MultipartPhase,
905 buffer: bytes::BytesMut,
906}
907
908enum MultipartPhase {
909 NextPart,
911 Payload(u64),
913 Terminator,
915 Closing,
917 Done,
918}
919
920fn multipart_stream(
926 handle: tokio::fs::File,
927 parts: Vec<(Bytes, ByteRange)>,
928 closing: Bytes,
929) -> impl futures_util::Stream<Item = std::io::Result<Frame<Bytes>>> + Send {
930 let state = MultipartState {
931 handle,
932 parts: parts.into_iter(),
933 closing,
934 pending_header: Bytes::new(),
935 phase: MultipartPhase::NextPart,
936 buffer: bytes::BytesMut::new(),
937 };
938
939 futures_util::stream::unfold(state, |mut state| async move {
940 loop {
941 match std::mem::replace(&mut state.phase, MultipartPhase::Done) {
942 MultipartPhase::NextPart => match state.parts.next() {
943 Some((header, range)) => {
944 if let Err(err) = state.handle.seek(SeekFrom::Start(range.start)).await {
945 return Some((Err(err), state));
946 }
947 state.pending_header = header;
948 state.phase = MultipartPhase::Payload(range.len());
949 }
950 None => state.phase = MultipartPhase::Closing,
951 },
952 MultipartPhase::Payload(remaining) => {
953 if remaining == 0 {
954 state.phase = MultipartPhase::Terminator;
955 continue;
956 }
957
958 let limit = MULTIPART_CHUNK.min(remaining as usize);
960 state.buffer.clear();
961 state.buffer.reserve(limit);
962 let read = match state.handle.read_buf(&mut state.buffer).await {
963 Ok(read) => read,
964 Err(err) => return Some((Err(err), state)),
965 };
966 if read == 0 {
967 return Some((
968 Err(std::io::Error::new(
969 std::io::ErrorKind::UnexpectedEof,
970 "file changed size while serving byte ranges",
971 )),
972 state,
973 ));
974 }
975 let keep = read.min(limit);
976 state.buffer.truncate(keep);
977 let chunk = state.buffer.split().freeze();
978
979 let header = std::mem::take(&mut state.pending_header);
982 let frame = if header.is_empty() {
983 chunk
984 } else {
985 let mut combined = bytes::BytesMut::with_capacity(header.len() + chunk.len());
986 combined.extend_from_slice(&header);
987 combined.extend_from_slice(&chunk);
988 combined.freeze()
989 };
990
991 state.phase = MultipartPhase::Payload(remaining - keep as u64);
992 return Some((Ok(Frame::data(frame)), state));
993 }
994 MultipartPhase::Terminator => {
995 state.phase = MultipartPhase::NextPart;
996 return Some((Ok(Frame::data(Bytes::from_static(b"\r\n"))), state));
997 }
998 MultipartPhase::Closing => {
999 state.phase = MultipartPhase::Done;
1000 let closing = std::mem::take(&mut state.closing);
1001 return Some((Ok(Frame::data(closing)), state));
1002 }
1003 MultipartPhase::Done => return None,
1004 }
1005 }
1006 })
1007}
1008
1009const STREAM_BUF_SIZE: usize = rust_webx_core::http::STREAM_BUF_SIZE;
1013
1014fn reader_body<R>(reader: R) -> RespBody
1016where
1017 R: tokio::io::AsyncRead + Send + 'static,
1018{
1019 let stream = tokio_util::io::ReaderStream::with_capacity(reader, STREAM_BUF_SIZE);
1020 let body = http_body_util::StreamBody::new(stream.map_ok(Frame::data));
1021 BodyExt::boxed_unsync(body)
1022}
1023
1024fn bytes_response(
1025 status: u16,
1026 headers: HashMap<String, String>,
1027 bytes: Vec<u8>,
1028) -> hyper::Response<RespBody> {
1029 build_response(status, headers, full_body(bytes))
1030}
1031
1032fn full_body(bytes: Vec<u8>) -> RespBody {
1033 Full::new(Bytes::from(bytes))
1034 .map_err(|never: std::convert::Infallible| match never {})
1035 .boxed_unsync()
1036}
1037
1038fn problem_bytes(
1040 status: u16,
1041 mut headers: HashMap<String, String>,
1042 detail: &str,
1043) -> hyper::Response<RespBody> {
1044 headers.insert(
1045 "content-type".to_string(),
1046 "application/problem+json".to_string(),
1047 );
1048 bytes_response(status, headers, problem_to_bytes(&build_problem(status, detail)))
1049}
1050
1051fn build_response(
1052 status: u16,
1053 headers: HashMap<String, String>,
1054 body: RespBody,
1055) -> hyper::Response<RespBody> {
1056 let mut builder = hyper::Response::builder().status(status);
1057 for (key, value) in &headers {
1058 builder = builder.header(key.as_str(), value.as_str());
1059 }
1060 builder.body(body).unwrap_or_else(|_| {
1061 hyper::Response::builder()
1062 .status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
1063 .body(full_body(b"Internal Server Error".to_vec()))
1064 .expect("static error response is valid")
1065 })
1066}
1067
1068#[derive(Debug, PartialEq, Eq)]
1070enum RangeOutcome {
1071 Ignored,
1073 Unsatisfiable,
1075 Satisfiable(Vec<ByteRange>),
1077}
1078
1079const MAX_RANGE_PARTS: usize = 8;
1086
1087fn parse_ranges(value: &str, len: u64) -> RangeOutcome {
1094 let value = value.trim();
1095 let Some(spec) = strip_prefix_ignore_ascii_case(value, "bytes=") else {
1096 return RangeOutcome::Ignored;
1097 };
1098
1099 let mut ranges: Vec<ByteRange> = Vec::new();
1100 let mut saw_valid_spec = false;
1101
1102 for part in spec.split(',') {
1103 let part = part.trim();
1104 if part.is_empty() {
1105 continue;
1106 }
1107 match parse_single_range(part, len) {
1108 SingleRange::Malformed => return RangeOutcome::Ignored,
1110 SingleRange::Unsatisfiable => saw_valid_spec = true,
1111 SingleRange::Satisfiable(range) => {
1112 saw_valid_spec = true;
1113 ranges.push(range);
1114 }
1115 }
1116 }
1117
1118 if ranges.is_empty() {
1119 return if saw_valid_spec {
1120 RangeOutcome::Unsatisfiable
1121 } else {
1122 RangeOutcome::Ignored
1123 };
1124 }
1125
1126 ranges.sort_by_key(|range| range.start);
1127 let mut merged: Vec<ByteRange> = Vec::with_capacity(ranges.len());
1128 for range in ranges {
1129 match merged.last_mut() {
1130 Some(last) if range.start <= last.end.saturating_add(1) => {
1132 last.end = last.end.max(range.end);
1133 }
1134 _ => merged.push(range),
1135 }
1136 }
1137
1138 if merged.len() > MAX_RANGE_PARTS {
1139 return RangeOutcome::Ignored;
1140 }
1141
1142 RangeOutcome::Satisfiable(merged)
1143}
1144
1145enum SingleRange {
1146 Malformed,
1148 Unsatisfiable,
1150 Satisfiable(ByteRange),
1151}
1152
1153fn parse_single_range(spec: &str, len: u64) -> SingleRange {
1155 let Some((start_text, end_text)) = spec.split_once('-') else {
1156 return SingleRange::Malformed;
1157 };
1158 let (start_text, end_text) = (start_text.trim(), end_text.trim());
1159
1160 if start_text.is_empty() {
1161 let Ok(suffix) = end_text.parse::<u64>() else {
1163 return SingleRange::Malformed;
1164 };
1165 if suffix == 0 || len == 0 {
1166 return SingleRange::Unsatisfiable;
1167 }
1168 return SingleRange::Satisfiable(ByteRange {
1169 start: len.saturating_sub(suffix),
1170 end: len - 1,
1171 });
1172 }
1173
1174 let Ok(start) = start_text.parse::<u64>() else {
1175 return SingleRange::Malformed;
1176 };
1177 if start >= len {
1178 return SingleRange::Unsatisfiable;
1179 }
1180
1181 let end = if end_text.is_empty() {
1182 len - 1
1183 } else {
1184 match end_text.parse::<u64>() {
1185 Ok(end) => end.min(len - 1),
1186 Err(_) => return SingleRange::Malformed,
1187 }
1188 };
1189
1190 if end < start {
1191 return SingleRange::Unsatisfiable;
1192 }
1193
1194 SingleRange::Satisfiable(ByteRange { start, end })
1195}
1196
1197fn strip_prefix_ignore_ascii_case<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
1198 if value.len() >= prefix.len() && value[..prefix.len()].eq_ignore_ascii_case(prefix) {
1199 Some(&value[prefix.len()..])
1200 } else {
1201 None
1202 }
1203}
1204
1205fn etag_for(len: u64, modified: Option<SystemTime>) -> String {
1217 match modified {
1218 Some(modified) => format!("\"{:x}-{:x}\"", len, epoch_nanos(modified)),
1219 None => format!("\"{len:x}\""),
1220 }
1221}
1222
1223fn epoch_secs(time: SystemTime) -> u64 {
1224 time.duration_since(UNIX_EPOCH)
1225 .map(|d| d.as_secs())
1226 .unwrap_or(0)
1227}
1228
1229fn epoch_nanos(time: SystemTime) -> u128 {
1230 time.duration_since(UNIX_EPOCH)
1231 .map(|d| d.as_nanos())
1232 .unwrap_or(0)
1233}
1234
1235fn is_weak_etag(tag: &str) -> bool {
1237 tag.trim_start().starts_with("W/")
1238}
1239
1240fn etag_value(tag: &str) -> &str {
1242 tag.trim().trim_start_matches("W/")
1243}
1244
1245fn is_not_modified(req: &HttpRequest, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
1251 if let Some(if_none_match) = req.header("if-none-match") {
1252 let Some(etag) = etag else {
1253 return false;
1254 };
1255 let expected = etag_value(etag);
1256 return if_none_match.split(',').any(|candidate| {
1257 let candidate = candidate.trim();
1258 candidate == "*" || etag_value(candidate) == expected
1259 });
1260 }
1261
1262 if let Some(if_modified_since) = req.header("if-modified-since") {
1263 if let Some(modified) = modified {
1264 if let Ok(since) = httpdate::parse_http_date(if_modified_since) {
1265 return epoch_secs(modified) <= epoch_secs(since);
1266 }
1267 }
1268 }
1269
1270 false
1271}
1272
1273fn if_range_matches(req: &HttpRequest, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
1284 let Some(value) = req.header("if-range") else {
1285 return true;
1286 };
1287 let value = value.trim();
1288
1289 if value.starts_with('"') || value.starts_with("W/") {
1290 let Some(etag) = etag else {
1291 return false;
1292 };
1293 if is_weak_etag(value) || is_weak_etag(etag) {
1294 return false;
1295 }
1296 return value == etag.trim();
1297 }
1298
1299 match (httpdate::parse_http_date(value), modified) {
1300 (Ok(instant), Some(modified)) => epoch_secs(instant) >= epoch_secs(modified),
1301 _ => false,
1302 }
1303}
1304
1305fn guess_content_type(path: &Path) -> Option<String> {
1306 mime_guess::from_path(path)
1307 .first()
1308 .map(|mime| mime.essence_str().to_string())
1309}
1310
1311fn parse_query_string(query: &str) -> HashMap<String, String> {
1317 let mut params = HashMap::new();
1318 for pair in query.split('&') {
1319 if pair.is_empty() {
1320 continue;
1321 }
1322 let mut parts = pair.splitn(2, '=');
1323 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
1324 params.insert(percent_decode(key), percent_decode(value));
1325 }
1326 }
1327 params
1328}
1329
1330fn percent_decode(s: &str) -> String {
1331 let mut result = String::with_capacity(s.len());
1332 let mut chars = s.chars();
1333 while let Some(c) = chars.next() {
1334 if c == '%' {
1335 let hex: String = chars.by_ref().take(2).collect();
1336 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
1337 result.push(byte as char);
1338 }
1339 } else if c == '+' {
1340 result.push(' ');
1341 } else {
1342 result.push(c);
1343 }
1344 }
1345 result
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 #[test]
1353 fn parses_the_boundary_parameter() {
1354 assert_eq!(
1355 multipart_boundary("multipart/form-data; boundary=----abc123").as_deref(),
1356 Some("----abc123")
1357 );
1358 assert_eq!(
1359 multipart_boundary("multipart/form-data; charset=utf-8; boundary=\"quoted\"").as_deref(),
1360 Some("quoted")
1361 );
1362 assert_eq!(multipart_boundary("multipart/form-data"), None);
1363 }
1364
1365 fn ranges(start: u64, end: u64) -> RangeOutcome {
1366 RangeOutcome::Satisfiable(vec![ByteRange { start, end }])
1367 }
1368
1369 #[test]
1370 fn parses_byte_ranges() {
1371 assert_eq!(parse_ranges("bytes=0-99", 1000), ranges(0, 99));
1372 assert_eq!(parse_ranges("bytes=500-", 1000), ranges(500, 999));
1373 assert_eq!(parse_ranges("bytes=-200", 1000), ranges(800, 999));
1374 assert_eq!(parse_ranges("bytes=0-99999", 1000), ranges(0, 999));
1376 assert_eq!(parse_ranges("BYTES=0-9", 1000), ranges(0, 9));
1378 assert_eq!(parse_ranges("bytes= 0-9 ", 1000), ranges(0, 9));
1379 }
1380
1381 #[test]
1382 fn rejects_unsatisfiable_and_malformed_ranges() {
1383 assert_eq!(parse_ranges("bytes=1000-", 1000), RangeOutcome::Unsatisfiable);
1384 assert_eq!(parse_ranges("bytes=-0", 1000), RangeOutcome::Unsatisfiable);
1385 assert_eq!(
1386 parse_ranges("bytes=500-100", 1000),
1387 RangeOutcome::Unsatisfiable
1388 );
1389 assert_eq!(parse_ranges("items=0-10", 1000), RangeOutcome::Ignored);
1390 assert_eq!(parse_ranges("bytes=", 1000), RangeOutcome::Ignored);
1391 assert_eq!(parse_ranges("bytes=abc-def", 1000), RangeOutcome::Ignored);
1393 assert_eq!(parse_ranges("bytes=0-9,abc-def", 1000), RangeOutcome::Ignored);
1394 }
1395
1396 #[test]
1397 fn suffix_range_on_empty_file_is_unsatisfiable() {
1398 assert_eq!(parse_ranges("bytes=-10", 0), RangeOutcome::Unsatisfiable);
1399 }
1400
1401 #[test]
1402 fn multi_range_is_coalesced_into_disjoint_parts() {
1403 assert_eq!(
1405 parse_ranges("bytes=20-30,0-10", 1000),
1406 RangeOutcome::Satisfiable(vec![
1407 ByteRange { start: 0, end: 10 },
1408 ByteRange { start: 20, end: 30 },
1409 ])
1410 );
1411
1412 assert_eq!(
1415 parse_ranges("bytes=0-99,50-149", 1000),
1416 ranges(0, 149)
1417 );
1418
1419 assert_eq!(parse_ranges("bytes=0-9,10-19", 1000), ranges(0, 19));
1421
1422 assert_eq!(
1424 parse_ranges("bytes=0-,0-,0-,0-,0-,0-,0-,0-", 1000),
1425 ranges(0, 999)
1426 );
1427
1428 assert_eq!(parse_ranges("bytes=0-9,5000-6000", 1000), ranges(0, 9));
1430 }
1431
1432 #[test]
1433 fn too_many_parts_falls_back_to_the_full_representation() {
1434 let mut spec = String::from("bytes=");
1435 for index in 0..(MAX_RANGE_PARTS + 1) {
1436 if index > 0 {
1437 spec.push(',');
1438 }
1439 spec.push_str(&format!("{}-{}", index * 4, index * 4 + 1));
1440 }
1441 assert_eq!(parse_ranges(&spec, 1000), RangeOutcome::Ignored);
1442 }
1443
1444 #[test]
1445 fn etag_changes_with_size_and_mtime() {
1446 let base = UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000);
1447 assert_eq!(etag_for(10, Some(base)), etag_for(10, Some(base)));
1448 assert_ne!(etag_for(10, Some(base)), etag_for(11, Some(base)));
1449 assert_ne!(
1450 etag_for(10, Some(base)),
1451 etag_for(10, Some(base + std::time::Duration::from_secs(1)))
1452 );
1453 assert_ne!(
1457 etag_for(10, Some(base)),
1458 etag_for(10, Some(base + std::time::Duration::from_micros(1)))
1459 );
1460 }
1461
1462 #[test]
1463 fn etag_strength_rules() {
1464 assert!(!is_weak_etag("\"abc\""));
1465 assert!(is_weak_etag("W/\"abc\""));
1466 assert_eq!(etag_value("W/\"abc\""), "\"abc\"");
1467 assert_eq!(etag_value("\"abc\""), "\"abc\"");
1468 }
1469
1470 #[test]
1471 fn parses_query_strings() {
1472 let params = parse_query_string("a=1&b=hello+world&c=%2Ftmp");
1473 assert_eq!(params.get("a").map(String::as_str), Some("1"));
1474 assert_eq!(params.get("b").map(String::as_str), Some("hello world"));
1475 assert_eq!(params.get("c").map(String::as_str), Some("/tmp"));
1476 }
1477}