1use futures_util::{StreamExt, TryStreamExt};
26use http_body_util::{combinators::UnsyncBoxBody, BodyExt, Full};
27use hyper::body::{Bytes, Frame, Incoming};
28use hyper::Request;
29use std::collections::HashMap;
30use std::path::{Path, PathBuf};
31use std::sync::Arc;
32use std::time::{SystemTime, UNIX_EPOCH};
33use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, SeekFrom};
34use tokio::sync::Mutex;
35use webx_core::auth::IClaims;
36use webx_core::config::AppOptions;
37use webx_core::error::{Error, Result};
38use webx_core::form::{FormFileBuilder, MultipartForm};
39use webx_core::http::{
40 content_disposition_value, ByteRange, FileBody, FileSource, IClaimsExt, IHttpContext,
41 IHttpRequest, IHttpResponse, ResponseBody, SeekableReader,
42};
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: 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 webx_core::paths::app_base().join(path)
98 }
99 }
100 _ => 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 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 {
186 req,
187 resp,
188 claims: None,
189 }
190 }
191
192 pub async fn into_response(self) -> hyper::Response<RespBody> {
194 let HttpContext { req, resp, .. } = self;
195 finalize_response(&req, resp).await
196 }
197}
198
199impl IClaimsExt for HttpContext {
200 fn set_claims(&mut self, claims: Box<dyn IClaims>) {
201 self.claims = Some(claims);
202 }
203
204 fn claims(&self) -> Option<&dyn IClaims> {
205 self.claims.as_deref()
206 }
207}
208
209impl IHttpContext for HttpContext {
210 fn request(&self) -> &dyn IHttpRequest {
211 &self.req
212 }
213
214 fn request_mut(&mut self) -> &mut dyn IHttpRequest {
215 &mut self.req
216 }
217
218 fn response(&self) -> &dyn IHttpResponse {
219 &self.resp
220 }
221
222 fn response_mut(&mut self) -> &mut dyn IHttpResponse {
223 &mut self.resp
224 }
225}
226
227enum RequestBody {
233 Pending(Option<Incoming>),
235 Buffered(Vec<u8>),
237 Form(Arc<MultipartForm>),
239}
240
241pub struct HttpRequest {
243 method: String,
244 path: String,
245 headers: HashMap<String, String>,
246 query_params: HashMap<String, String>,
247 route_params: HashMap<String, String>,
248 route_pattern: Option<String>,
249 body: Mutex<RequestBody>,
250 limits: BodyLimits,
251}
252
253#[async_trait::async_trait]
254impl IHttpRequest for HttpRequest {
255 fn method(&self) -> &str {
256 &self.method
257 }
258
259 fn path(&self) -> &str {
260 &self.path
261 }
262
263 fn header(&self, name: &str) -> Option<&str> {
264 self.headers.get(name).map(|s| s.as_str())
265 }
266
267 fn query(&self) -> &HashMap<String, String> {
268 &self.query_params
269 }
270
271 fn route_params(&self) -> &HashMap<String, String> {
272 &self.route_params
273 }
274
275 fn route_params_mut(&mut self) -> &mut HashMap<String, String> {
276 &mut self.route_params
277 }
278
279 fn route_pattern(&self) -> Option<&str> {
280 self.route_pattern.as_deref()
281 }
282
283 fn route_pattern_mut(&mut self) -> &mut Option<String> {
284 &mut self.route_pattern
285 }
286
287 async fn body_bytes(&mut self) -> Result<Vec<u8>> {
288 let mut guard = self.body.lock().await;
289 let incoming = match &mut *guard {
290 RequestBody::Buffered(bytes) => return Ok(bytes.clone()),
291 RequestBody::Form(_) => {
292 return Err(Error::Http(
293 "request body was already read as multipart/form-data".to_string(),
294 ))
295 }
296 RequestBody::Pending(slot) => slot.take(),
297 };
298
299 let Some(incoming) = incoming else {
300 return Err(Error::Http(
301 "request body has already been consumed".to_string(),
302 ));
303 };
304
305 let bytes = read_body(incoming, self.limits.body).await?;
306 *guard = RequestBody::Buffered(bytes.clone());
307 Ok(bytes)
308 }
309
310 async fn body_text(&mut self) -> Result<String> {
311 let bytes = self.body_bytes().await?;
312 String::from_utf8(bytes).map_err(|e| Error::Http(e.to_string()))
313 }
314
315 async fn multipart(&mut self) -> Result<Arc<MultipartForm>> {
316 let mut guard = self.body.lock().await;
317
318 match &*guard {
319 RequestBody::Form(form) => return Ok(Arc::clone(form)),
320 RequestBody::Buffered(_) => {
321 return Err(Error::UnsupportedMediaType(
322 "request body was already read as a non-multipart body".to_string(),
323 ))
324 }
325 RequestBody::Pending(_) => {}
326 }
327
328 if !self.is_multipart() {
329 return Err(Error::UnsupportedMediaType(
330 "expected Content-Type: multipart/form-data".to_string(),
331 ));
332 }
333
334 let raw_content_type = self.header("content-type").unwrap_or("");
335 let boundary = multipart_boundary(raw_content_type).ok_or_else(|| {
336 Error::Validation(
337 "multipart/form-data request is missing its boundary parameter".to_string(),
338 )
339 })?;
340
341 let incoming = match &mut *guard {
342 RequestBody::Pending(slot) => slot.take(),
343 _ => None,
344 };
345
346 let Some(body) = incoming else {
347 return Err(Error::Http(
348 "request body has already been consumed".to_string(),
349 ));
350 };
351
352 let form = Arc::new(parse_multipart(body, &boundary, &self.limits).await?);
353 *guard = RequestBody::Form(Arc::clone(&form));
354 Ok(form)
355 }
356}
357
358async fn read_body(incoming: Incoming, limit: usize) -> Result<Vec<u8>> {
360 let mut out = Vec::new();
361 let mut total = 0usize;
362 let mut stream = incoming.into_data_stream();
363
364 while let Some(chunk) = stream.next().await {
365 let chunk =
366 chunk.map_err(|err| Error::Http(format!("failed to read request body: {err}")))?;
367 total = total.saturating_add(chunk.len());
368 if total > limit {
369 return Err(Error::PayloadTooLarge(format!(
370 "request body exceeds the {limit}-byte limit"
371 )));
372 }
373 out.extend_from_slice(&chunk);
374 }
375
376 Ok(out)
377}
378
379fn is_multipart_content_type(content_type: Option<&String>) -> bool {
381 content_type
382 .map(|value| {
383 value
384 .split(';')
385 .next()
386 .unwrap_or("")
387 .trim()
388 .eq_ignore_ascii_case("multipart/form-data")
389 })
390 .unwrap_or(false)
391}
392
393fn multipart_boundary(content_type: &str) -> Option<String> {
395 for part in content_type.split(';').skip(1) {
396 let part = part.trim();
397 let Some((key, value)) = part.split_once('=') else {
398 continue;
399 };
400 if key.trim().eq_ignore_ascii_case("boundary") {
401 let value = value.trim().trim_matches('"');
402 if !value.is_empty() {
403 return Some(value.to_string());
404 }
405 }
406 }
407 None
408}
409
410async fn parse_multipart(
417 incoming: Incoming,
418 boundary: &str,
419 limits: &BodyLimits,
420) -> Result<MultipartForm> {
421 let stream = incoming.into_data_stream();
422 let mut multipart = multer::Multipart::new(stream, boundary);
423 let mut form = MultipartForm::new();
424 let mut total: u64 = 0;
425
426 while let Some(field) = multipart.next_field().await.map_err(multer_error)? {
427 let field_name = field.name().unwrap_or_default().to_string();
428 if field_name.is_empty() {
429 continue;
430 }
431 let file_name = field.file_name().map(str::to_string);
432 let content_type = field.content_type().map(|m| m.to_string());
433
434 match file_name {
435 Some(file_name) => {
436 let mut builder = FormFileBuilder::new(field_name.clone(), file_name, content_type)
437 .memory_threshold(limits.memory_threshold)
438 .max_size(limits.file)
439 .spool_dir(limits.spool_dir.clone());
440
441 let mut field = field;
442 while let Some(chunk) = field.chunk().await.map_err(multer_error)? {
443 total = charge(&mut total, chunk.len(), limits.multipart_body)?;
444 builder.write(&chunk).await?;
445 }
446 form.push_file(builder.finish().await?);
447 }
448 None => {
449 let mut value: Vec<u8> = Vec::new();
450 let mut field = field;
451 while let Some(chunk) = field.chunk().await.map_err(multer_error)? {
452 total = charge(&mut total, chunk.len(), limits.multipart_body)?;
453 if value.len() + chunk.len() > limits.field {
454 return Err(Error::PayloadTooLarge(format!(
455 "form field `{field_name}` exceeds the {}-byte limit",
456 limits.field
457 )));
458 }
459 value.extend_from_slice(&chunk);
460 }
461 let text = String::from_utf8(value).map_err(|_| {
462 Error::Validation(format!("form field `{field_name}` is not valid UTF-8"))
463 })?;
464 form.push_field(field_name, text);
465 }
466 }
467 }
468
469 Ok(form)
470}
471
472fn charge(total: &mut u64, len: usize, limit: u64) -> Result<u64> {
474 *total = total.saturating_add(len as u64);
475 if *total > limit {
476 return Err(Error::PayloadTooLarge(format!(
477 "multipart request exceeds the {limit}-byte limit"
478 )));
479 }
480 Ok(*total)
481}
482
483fn multer_error(err: multer::Error) -> Error {
484 match err {
485 multer::Error::FieldSizeExceeded { limit, .. } => {
486 Error::PayloadTooLarge(format!("multipart field exceeds the {limit}-byte limit"))
487 }
488 multer::Error::StreamSizeExceeded { limit } => {
489 Error::PayloadTooLarge(format!("multipart request exceeds the {limit}-byte limit"))
490 }
491 multer::Error::IncompleteFieldData { .. } | multer::Error::IncompleteStream => {
492 Error::Validation("multipart body ended unexpectedly".to_string())
493 }
494 other => Error::Validation(format!("invalid multipart body: {other}")),
495 }
496}
497
498pub struct HttpResponse {
504 status: u16,
505 headers: HashMap<String, String>,
506 body: Option<ResponseBody>,
507}
508
509impl HttpResponse {
510 pub fn new(status: u16) -> Self {
511 Self {
512 status,
513 headers: HashMap::new(),
514 body: None,
515 }
516 }
517}
518
519#[async_trait::async_trait]
520impl IHttpResponse for HttpResponse {
521 fn status(&self) -> u16 {
522 self.status
523 }
524
525 fn has_body(&self) -> bool {
526 self.body.is_some()
527 }
528
529 fn set_status(&mut self, code: u16) {
530 self.status = code;
531 }
532
533 fn set_header(&mut self, key: &str, value: &str) {
534 self.headers.insert(key.to_string(), value.to_string());
535 }
536
537 fn remove_header(&mut self, key: &str) {
538 self.headers.remove(key);
539 }
540
541 async fn write_bytes(&mut self, data: Vec<u8>) -> Result<()> {
542 self.body = Some(ResponseBody::Bytes(data));
543 Ok(())
544 }
545
546 async fn write_text(&mut self, text: &str) -> Result<()> {
547 self.body = Some(ResponseBody::Bytes(text.as_bytes().to_vec()));
548 Ok(())
549 }
550
551 async fn write_body(&mut self, body: ResponseBody) -> Result<()> {
552 self.body = Some(body);
553 Ok(())
554 }
555
556 fn body_bytes(&self) -> Vec<u8> {
557 self.body
558 .as_ref()
559 .and_then(ResponseBody::as_bytes)
560 .map(<[u8]>::to_vec)
561 .unwrap_or_default()
562 }
563
564 fn header(&self, key: &str) -> Option<&str> {
565 self.headers.get(key).map(|s| s.as_str())
566 }
567}
568
569async fn finalize_response(req: &HttpRequest, resp: HttpResponse) -> hyper::Response<RespBody> {
574 let HttpResponse {
575 status,
576 mut headers,
577 body,
578 } = resp;
579
580 match body.unwrap_or(ResponseBody::Bytes(Vec::new())) {
581 ResponseBody::Bytes(bytes) => {
582 default_cache_control(&mut headers, false);
583 bytes_response(status, headers, bytes)
584 }
585 ResponseBody::File(file) => {
586 default_cache_control(&mut headers, true);
587 file_response(req, status, headers, file).await
588 }
589 }
590}
591
592fn default_cache_control(headers: &mut HashMap<String, String>, is_file: bool) {
598 if headers.contains_key("cache-control") {
599 return;
600 }
601 headers.insert(
602 "cache-control".to_string(),
603 if is_file {
604 "public, max-age=0, must-revalidate".to_string()
605 } else {
606 "no-store".to_string()
607 },
608 );
609}
610
611enum PreparedSource {
613 Path(tokio::fs::File),
615 Seekable(Box<dyn SeekableReader>),
617 Stream(Box<dyn AsyncRead + Send + Unpin>),
619}
620
621impl PreparedSource {
622 fn is_seekable(&self) -> bool {
623 matches!(self, PreparedSource::Path(_) | PreparedSource::Seekable(_))
624 }
625}
626
627async fn file_response(
632 req: &HttpRequest,
633 status: u16,
634 mut headers: HashMap<String, String>,
635 file: FileBody,
636) -> hyper::Response<RespBody> {
637 let FileBody {
638 source,
639 content_type,
640 file_name,
641 disposition,
642 enable_range_processing,
643 last_modified,
644 entity_tag,
645 } = file;
646
647 let mut modified = last_modified;
649 let mut etag = entity_tag;
650 let mut source_path: Option<PathBuf> = None;
651 let known_len;
652
653 let prepared = match source {
654 FileSource::Path(path) => {
655 let handle = match tokio::fs::File::open(&path).await {
658 Ok(handle) => handle,
659 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
660 return problem_bytes(404, headers, "Not Found")
661 }
662 Err(err) => {
663 return problem_bytes(
664 500,
665 headers,
666 &format!("cannot open `{}`: {err}", path.display()),
667 )
668 }
669 };
670 let metadata = match handle.metadata().await {
671 Ok(metadata) => metadata,
672 Err(err) => {
673 return problem_bytes(
674 500,
675 headers,
676 &format!("cannot stat `{}`: {err}", path.display()),
677 )
678 }
679 };
680 if !metadata.is_file() {
681 return problem_bytes(404, headers, "Not Found");
682 }
683
684 let len = metadata.len();
685 if modified.is_none() {
686 modified = metadata.modified().ok();
687 }
688 if etag.is_none() {
689 etag = Some(etag_for(len, modified));
690 }
691 known_len = Some(len);
692 source_path = Some(path);
693 PreparedSource::Path(handle)
694 }
695 FileSource::SeekableStream { reader, len } => {
696 known_len = Some(len);
697 PreparedSource::Seekable(reader)
698 }
699 FileSource::Stream(reader) => {
700 known_len = None;
701 PreparedSource::Stream(reader)
702 }
703 };
704
705 let modified = modified.filter(|value| *value <= SystemTime::now());
712
713 if !headers.contains_key("content-type") {
715 let resolved = content_type
716 .or_else(|| source_path.as_deref().and_then(guess_content_type))
717 .unwrap_or_else(|| "application/octet-stream".to_string());
718 headers.insert("content-type".to_string(), resolved);
719 }
720 if !headers.contains_key("content-disposition") {
721 if let Some(name) = &file_name {
722 headers.insert(
723 "content-disposition".to_string(),
724 content_disposition_value(disposition, name),
725 );
726 }
727 }
728 if let Some(tag) = &etag {
729 headers.insert("etag".to_string(), tag.clone());
730 }
731 if let Some(modified) = modified {
732 headers.insert(
733 "last-modified".to_string(),
734 httpdate::fmt_http_date(modified),
735 );
736 }
737
738 let range_capable = enable_range_processing && prepared.is_seekable() && known_len.is_some();
741 headers.insert(
742 "accept-ranges".to_string(),
743 if range_capable { "bytes" } else { "none" }.to_string(),
744 );
745
746 if status == 200 && is_not_modified(req, etag.as_deref(), modified) {
748 headers.remove("content-length");
749 return bytes_response(304, headers, Vec::new());
750 }
751
752 let mut ranges: Vec<ByteRange> = Vec::new();
754 if status == 200 && range_capable {
755 let len = known_len.expect("range-capable sources know their length");
756 if let Some(raw) = req.header("range") {
757 if if_range_matches(req, etag.as_deref(), modified) {
761 match parse_ranges(raw, len) {
762 RangeOutcome::Ignored => {}
763 RangeOutcome::Unsatisfiable => {
764 headers.insert("content-range".to_string(), format!("bytes */{len}"));
765 return problem_bytes(416, headers, "Range Not Satisfiable");
766 }
767 RangeOutcome::Satisfiable(parsed) => ranges = parsed,
768 }
769 }
770 }
771 }
772
773 if ranges.len() > 1 && !matches!(prepared, PreparedSource::Path(_)) {
780 ranges.clear();
781 }
782 if ranges.len() > 1 {
783 return multipart_byteranges_response(prepared, headers, ranges, known_len);
784 }
785
786 let (status, start, count) = match ranges.first() {
787 Some(range) => {
788 let len = known_len.expect("range-capable sources know their length");
789 headers.insert(
790 "content-range".to_string(),
791 format!("bytes {}-{}/{len}", range.start, range.end),
792 );
793 (206, range.start, range.len())
794 }
795 None => (status, 0, known_len.unwrap_or(0)),
796 };
797
798 match prepared {
799 PreparedSource::Path(mut handle) => {
800 headers.insert("content-length".to_string(), count.to_string());
801 if count == 0 {
802 return bytes_response(status, headers, Vec::new());
803 }
804 if start > 0 {
805 if let Err(err) = handle.seek(SeekFrom::Start(start)).await {
806 return problem_bytes(500, headers, &format!("cannot seek: {err}"));
807 }
808 }
809 build_response(status, headers, reader_body(handle.take(count)))
810 }
811 PreparedSource::Seekable(mut reader) => {
812 headers.insert("content-length".to_string(), count.to_string());
813 if count == 0 {
814 return bytes_response(status, headers, Vec::new());
815 }
816 if start > 0 {
817 if let Err(err) = reader.seek(SeekFrom::Start(start)).await {
818 return problem_bytes(500, headers, &format!("cannot seek: {err}"));
819 }
820 }
821 build_response(status, headers, reader_body(reader.take(count)))
822 }
823 PreparedSource::Stream(reader) => {
824 build_response(status, headers, reader_body(reader))
827 }
828 }
829}
830
831fn representation_content_type(headers: &HashMap<String, String>) -> String {
836 headers
837 .get("content-type")
838 .filter(|value| !value.starts_with("multipart/byteranges"))
839 .cloned()
840 .unwrap_or_else(|| "application/octet-stream".to_string())
841}
842
843fn multipart_byteranges_response(
850 prepared: PreparedSource,
851 mut headers: HashMap<String, String>,
852 ranges: Vec<ByteRange>,
853 known_len: Option<u64>,
854) -> hyper::Response<RespBody> {
855 let PreparedSource::Path(handle) = prepared else {
856 unreachable!("multi-range is only routed here for path sources");
857 };
858
859 let total_len = known_len.unwrap_or(0);
860 let media_type = representation_content_type(&headers);
861 let boundary = format!("webx-{}", uuid::Uuid::new_v4().simple());
862
863 let parts: Vec<(Bytes, ByteRange)> = ranges
866 .iter()
867 .map(|range| {
868 let header = format!(
869 "--{boundary}\r\nContent-Type: {media_type}\r\nContent-Range: bytes {}-{}/{total_len}\r\n\r\n",
870 range.start, range.end
871 );
872 (Bytes::from(header), *range)
873 })
874 .collect();
875 let closing = Bytes::from(format!("--{boundary}--\r\n"));
876
877 let content_length: u64 = parts
878 .iter()
879 .map(|(header, range)| header.len() as u64 + range.len() + 2)
880 .sum::<u64>()
881 + closing.len() as u64;
882
883 headers.insert(
884 "content-type".to_string(),
885 format!("multipart/byteranges; boundary={boundary}"),
886 );
887 headers.insert("content-length".to_string(), content_length.to_string());
888 headers.remove("content-range");
890
891 let stream = multipart_stream(handle, parts, closing);
892 build_response(
893 206,
894 headers,
895 BodyExt::boxed_unsync(http_body_util::StreamBody::new(stream)),
896 )
897}
898
899const MULTIPART_CHUNK: usize = STREAM_BUF_SIZE;
901
902struct MultipartState {
904 handle: tokio::fs::File,
905 parts: std::vec::IntoIter<(Bytes, ByteRange)>,
906 closing: Bytes,
907 pending_header: Bytes,
909 phase: MultipartPhase,
910 buffer: bytes::BytesMut,
911}
912
913enum MultipartPhase {
914 NextPart,
916 Payload(u64),
918 Terminator,
920 Closing,
922 Done,
923}
924
925fn multipart_stream(
931 handle: tokio::fs::File,
932 parts: Vec<(Bytes, ByteRange)>,
933 closing: Bytes,
934) -> impl futures_util::Stream<Item = std::io::Result<Frame<Bytes>>> + Send {
935 let state = MultipartState {
936 handle,
937 parts: parts.into_iter(),
938 closing,
939 pending_header: Bytes::new(),
940 phase: MultipartPhase::NextPart,
941 buffer: bytes::BytesMut::new(),
942 };
943
944 futures_util::stream::unfold(state, |mut state| async move {
945 loop {
946 match std::mem::replace(&mut state.phase, MultipartPhase::Done) {
947 MultipartPhase::NextPart => match state.parts.next() {
948 Some((header, range)) => {
949 if let Err(err) = state.handle.seek(SeekFrom::Start(range.start)).await {
950 return Some((Err(err), state));
951 }
952 state.pending_header = header;
953 state.phase = MultipartPhase::Payload(range.len());
954 }
955 None => state.phase = MultipartPhase::Closing,
956 },
957 MultipartPhase::Payload(remaining) => {
958 if remaining == 0 {
959 state.phase = MultipartPhase::Terminator;
960 continue;
961 }
962
963 let limit = MULTIPART_CHUNK.min(remaining as usize);
965 state.buffer.clear();
966 state.buffer.reserve(limit);
967 let read = match state.handle.read_buf(&mut state.buffer).await {
968 Ok(read) => read,
969 Err(err) => return Some((Err(err), state)),
970 };
971 if read == 0 {
972 return Some((
973 Err(std::io::Error::new(
974 std::io::ErrorKind::UnexpectedEof,
975 "file changed size while serving byte ranges",
976 )),
977 state,
978 ));
979 }
980 let keep = read.min(limit);
981 state.buffer.truncate(keep);
982 let chunk = state.buffer.split().freeze();
983
984 let header = std::mem::take(&mut state.pending_header);
987 let frame = if header.is_empty() {
988 chunk
989 } else {
990 let mut combined =
991 bytes::BytesMut::with_capacity(header.len() + chunk.len());
992 combined.extend_from_slice(&header);
993 combined.extend_from_slice(&chunk);
994 combined.freeze()
995 };
996
997 state.phase = MultipartPhase::Payload(remaining - keep as u64);
998 return Some((Ok(Frame::data(frame)), state));
999 }
1000 MultipartPhase::Terminator => {
1001 state.phase = MultipartPhase::NextPart;
1002 return Some((Ok(Frame::data(Bytes::from_static(b"\r\n"))), state));
1003 }
1004 MultipartPhase::Closing => {
1005 state.phase = MultipartPhase::Done;
1006 let closing = std::mem::take(&mut state.closing);
1007 return Some((Ok(Frame::data(closing)), state));
1008 }
1009 MultipartPhase::Done => return None,
1010 }
1011 }
1012 })
1013}
1014
1015const STREAM_BUF_SIZE: usize = webx_core::http::STREAM_BUF_SIZE;
1019
1020fn reader_body<R>(reader: R) -> RespBody
1022where
1023 R: tokio::io::AsyncRead + Send + 'static,
1024{
1025 let stream = tokio_util::io::ReaderStream::with_capacity(reader, STREAM_BUF_SIZE);
1026 let body = http_body_util::StreamBody::new(stream.map_ok(Frame::data));
1027 BodyExt::boxed_unsync(body)
1028}
1029
1030fn bytes_response(
1031 status: u16,
1032 headers: HashMap<String, String>,
1033 bytes: Vec<u8>,
1034) -> hyper::Response<RespBody> {
1035 build_response(status, headers, full_body(bytes))
1036}
1037
1038fn full_body(bytes: Vec<u8>) -> RespBody {
1039 Full::new(Bytes::from(bytes))
1040 .map_err(|never: std::convert::Infallible| match never {})
1041 .boxed_unsync()
1042}
1043
1044fn problem_bytes(
1046 status: u16,
1047 mut headers: HashMap<String, String>,
1048 detail: &str,
1049) -> hyper::Response<RespBody> {
1050 headers.insert(
1051 "content-type".to_string(),
1052 "application/problem+json".to_string(),
1053 );
1054 bytes_response(
1055 status,
1056 headers,
1057 problem_to_bytes(&build_problem(status, detail)),
1058 )
1059}
1060
1061fn build_response(
1062 status: u16,
1063 headers: HashMap<String, String>,
1064 body: RespBody,
1065) -> hyper::Response<RespBody> {
1066 let mut builder = hyper::Response::builder().status(status);
1067 for (key, value) in &headers {
1068 builder = builder.header(key.as_str(), value.as_str());
1069 }
1070 builder.body(body).unwrap_or_else(|_| {
1071 hyper::Response::builder()
1072 .status(hyper::StatusCode::INTERNAL_SERVER_ERROR)
1073 .body(full_body(b"Internal Server Error".to_vec()))
1074 .expect("static error response is valid")
1075 })
1076}
1077
1078#[derive(Debug, PartialEq, Eq)]
1080enum RangeOutcome {
1081 Ignored,
1083 Unsatisfiable,
1085 Satisfiable(Vec<ByteRange>),
1087}
1088
1089const MAX_RANGE_PARTS: usize = 8;
1096
1097fn parse_ranges(value: &str, len: u64) -> RangeOutcome {
1104 let value = value.trim();
1105 let Some(spec) = strip_prefix_ignore_ascii_case(value, "bytes=") else {
1106 return RangeOutcome::Ignored;
1107 };
1108
1109 let mut ranges: Vec<ByteRange> = Vec::new();
1110 let mut saw_valid_spec = false;
1111
1112 for part in spec.split(',') {
1113 let part = part.trim();
1114 if part.is_empty() {
1115 continue;
1116 }
1117 match parse_single_range(part, len) {
1118 SingleRange::Malformed => return RangeOutcome::Ignored,
1120 SingleRange::Unsatisfiable => saw_valid_spec = true,
1121 SingleRange::Satisfiable(range) => {
1122 saw_valid_spec = true;
1123 ranges.push(range);
1124 }
1125 }
1126 }
1127
1128 if ranges.is_empty() {
1129 return if saw_valid_spec {
1130 RangeOutcome::Unsatisfiable
1131 } else {
1132 RangeOutcome::Ignored
1133 };
1134 }
1135
1136 ranges.sort_by_key(|range| range.start);
1137 let mut merged: Vec<ByteRange> = Vec::with_capacity(ranges.len());
1138 for range in ranges {
1139 match merged.last_mut() {
1140 Some(last) if range.start <= last.end.saturating_add(1) => {
1142 last.end = last.end.max(range.end);
1143 }
1144 _ => merged.push(range),
1145 }
1146 }
1147
1148 if merged.len() > MAX_RANGE_PARTS {
1149 return RangeOutcome::Ignored;
1150 }
1151
1152 RangeOutcome::Satisfiable(merged)
1153}
1154
1155enum SingleRange {
1156 Malformed,
1158 Unsatisfiable,
1160 Satisfiable(ByteRange),
1161}
1162
1163fn parse_single_range(spec: &str, len: u64) -> SingleRange {
1165 let Some((start_text, end_text)) = spec.split_once('-') else {
1166 return SingleRange::Malformed;
1167 };
1168 let (start_text, end_text) = (start_text.trim(), end_text.trim());
1169
1170 if start_text.is_empty() {
1171 let Ok(suffix) = end_text.parse::<u64>() else {
1173 return SingleRange::Malformed;
1174 };
1175 if suffix == 0 || len == 0 {
1176 return SingleRange::Unsatisfiable;
1177 }
1178 return SingleRange::Satisfiable(ByteRange {
1179 start: len.saturating_sub(suffix),
1180 end: len - 1,
1181 });
1182 }
1183
1184 let Ok(start) = start_text.parse::<u64>() else {
1185 return SingleRange::Malformed;
1186 };
1187 if start >= len {
1188 return SingleRange::Unsatisfiable;
1189 }
1190
1191 let end = if end_text.is_empty() {
1192 len - 1
1193 } else {
1194 match end_text.parse::<u64>() {
1195 Ok(end) => end.min(len - 1),
1196 Err(_) => return SingleRange::Malformed,
1197 }
1198 };
1199
1200 if end < start {
1201 return SingleRange::Unsatisfiable;
1202 }
1203
1204 SingleRange::Satisfiable(ByteRange { start, end })
1205}
1206
1207fn strip_prefix_ignore_ascii_case<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
1208 if value.len() >= prefix.len() && value[..prefix.len()].eq_ignore_ascii_case(prefix) {
1209 Some(&value[prefix.len()..])
1210 } else {
1211 None
1212 }
1213}
1214
1215fn etag_for(len: u64, modified: Option<SystemTime>) -> String {
1227 match modified {
1228 Some(modified) => format!("\"{:x}-{:x}\"", len, epoch_nanos(modified)),
1229 None => format!("\"{len:x}\""),
1230 }
1231}
1232
1233fn epoch_secs(time: SystemTime) -> u64 {
1234 time.duration_since(UNIX_EPOCH)
1235 .map(|d| d.as_secs())
1236 .unwrap_or(0)
1237}
1238
1239fn epoch_nanos(time: SystemTime) -> u128 {
1240 time.duration_since(UNIX_EPOCH)
1241 .map(|d| d.as_nanos())
1242 .unwrap_or(0)
1243}
1244
1245fn is_weak_etag(tag: &str) -> bool {
1247 tag.trim_start().starts_with("W/")
1248}
1249
1250fn etag_value(tag: &str) -> &str {
1252 tag.trim().trim_start_matches("W/")
1253}
1254
1255fn is_not_modified(req: &HttpRequest, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
1261 if let Some(if_none_match) = req.header("if-none-match") {
1262 let Some(etag) = etag else {
1263 return false;
1264 };
1265 let expected = etag_value(etag);
1266 return if_none_match.split(',').any(|candidate| {
1267 let candidate = candidate.trim();
1268 candidate == "*" || etag_value(candidate) == expected
1269 });
1270 }
1271
1272 if let Some(if_modified_since) = req.header("if-modified-since") {
1273 if let Some(modified) = modified {
1274 if let Ok(since) = httpdate::parse_http_date(if_modified_since) {
1275 return epoch_secs(modified) <= epoch_secs(since);
1276 }
1277 }
1278 }
1279
1280 false
1281}
1282
1283fn if_range_matches(req: &HttpRequest, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
1294 let Some(value) = req.header("if-range") else {
1295 return true;
1296 };
1297 let value = value.trim();
1298
1299 if value.starts_with('"') || value.starts_with("W/") {
1300 let Some(etag) = etag else {
1301 return false;
1302 };
1303 if is_weak_etag(value) || is_weak_etag(etag) {
1304 return false;
1305 }
1306 return value == etag.trim();
1307 }
1308
1309 match (httpdate::parse_http_date(value), modified) {
1310 (Ok(instant), Some(modified)) => epoch_secs(instant) >= epoch_secs(modified),
1311 _ => false,
1312 }
1313}
1314
1315fn guess_content_type(path: &Path) -> Option<String> {
1316 mime_guess::from_path(path)
1317 .first()
1318 .map(|mime| mime.essence_str().to_string())
1319}
1320
1321fn parse_query_string(query: &str) -> HashMap<String, String> {
1327 let mut params = HashMap::new();
1328 for pair in query.split('&') {
1329 if pair.is_empty() {
1330 continue;
1331 }
1332 let mut parts = pair.splitn(2, '=');
1333 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
1334 params.insert(percent_decode(key), percent_decode(value));
1335 }
1336 }
1337 params
1338}
1339
1340fn percent_decode(s: &str) -> String {
1341 let mut result = String::with_capacity(s.len());
1342 let mut chars = s.chars();
1343 while let Some(c) = chars.next() {
1344 if c == '%' {
1345 let hex: String = chars.by_ref().take(2).collect();
1346 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
1347 result.push(byte as char);
1348 }
1349 } else if c == '+' {
1350 result.push(' ');
1351 } else {
1352 result.push(c);
1353 }
1354 }
1355 result
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360 use super::*;
1361
1362 #[test]
1363 fn parses_the_boundary_parameter() {
1364 assert_eq!(
1365 multipart_boundary("multipart/form-data; boundary=----abc123").as_deref(),
1366 Some("----abc123")
1367 );
1368 assert_eq!(
1369 multipart_boundary("multipart/form-data; charset=utf-8; boundary=\"quoted\"")
1370 .as_deref(),
1371 Some("quoted")
1372 );
1373 assert_eq!(multipart_boundary("multipart/form-data"), None);
1374 }
1375
1376 fn ranges(start: u64, end: u64) -> RangeOutcome {
1377 RangeOutcome::Satisfiable(vec![ByteRange { start, end }])
1378 }
1379
1380 #[test]
1381 fn parses_byte_ranges() {
1382 assert_eq!(parse_ranges("bytes=0-99", 1000), ranges(0, 99));
1383 assert_eq!(parse_ranges("bytes=500-", 1000), ranges(500, 999));
1384 assert_eq!(parse_ranges("bytes=-200", 1000), ranges(800, 999));
1385 assert_eq!(parse_ranges("bytes=0-99999", 1000), ranges(0, 999));
1387 assert_eq!(parse_ranges("BYTES=0-9", 1000), ranges(0, 9));
1389 assert_eq!(parse_ranges("bytes= 0-9 ", 1000), ranges(0, 9));
1390 }
1391
1392 #[test]
1393 fn rejects_unsatisfiable_and_malformed_ranges() {
1394 assert_eq!(
1395 parse_ranges("bytes=1000-", 1000),
1396 RangeOutcome::Unsatisfiable
1397 );
1398 assert_eq!(parse_ranges("bytes=-0", 1000), RangeOutcome::Unsatisfiable);
1399 assert_eq!(
1400 parse_ranges("bytes=500-100", 1000),
1401 RangeOutcome::Unsatisfiable
1402 );
1403 assert_eq!(parse_ranges("items=0-10", 1000), RangeOutcome::Ignored);
1404 assert_eq!(parse_ranges("bytes=", 1000), RangeOutcome::Ignored);
1405 assert_eq!(parse_ranges("bytes=abc-def", 1000), RangeOutcome::Ignored);
1407 assert_eq!(
1408 parse_ranges("bytes=0-9,abc-def", 1000),
1409 RangeOutcome::Ignored
1410 );
1411 }
1412
1413 #[test]
1414 fn suffix_range_on_empty_file_is_unsatisfiable() {
1415 assert_eq!(parse_ranges("bytes=-10", 0), RangeOutcome::Unsatisfiable);
1416 }
1417
1418 #[test]
1419 fn multi_range_is_coalesced_into_disjoint_parts() {
1420 assert_eq!(
1422 parse_ranges("bytes=20-30,0-10", 1000),
1423 RangeOutcome::Satisfiable(vec![
1424 ByteRange { start: 0, end: 10 },
1425 ByteRange { start: 20, end: 30 },
1426 ])
1427 );
1428
1429 assert_eq!(parse_ranges("bytes=0-99,50-149", 1000), ranges(0, 149));
1432
1433 assert_eq!(parse_ranges("bytes=0-9,10-19", 1000), ranges(0, 19));
1435
1436 assert_eq!(
1438 parse_ranges("bytes=0-,0-,0-,0-,0-,0-,0-,0-", 1000),
1439 ranges(0, 999)
1440 );
1441
1442 assert_eq!(parse_ranges("bytes=0-9,5000-6000", 1000), ranges(0, 9));
1444 }
1445
1446 #[test]
1447 fn too_many_parts_falls_back_to_the_full_representation() {
1448 let mut spec = String::from("bytes=");
1449 for index in 0..(MAX_RANGE_PARTS + 1) {
1450 if index > 0 {
1451 spec.push(',');
1452 }
1453 spec.push_str(&format!("{}-{}", index * 4, index * 4 + 1));
1454 }
1455 assert_eq!(parse_ranges(&spec, 1000), RangeOutcome::Ignored);
1456 }
1457
1458 #[test]
1459 fn etag_changes_with_size_and_mtime() {
1460 let base = UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000);
1461 assert_eq!(etag_for(10, Some(base)), etag_for(10, Some(base)));
1462 assert_ne!(etag_for(10, Some(base)), etag_for(11, Some(base)));
1463 assert_ne!(
1464 etag_for(10, Some(base)),
1465 etag_for(10, Some(base + std::time::Duration::from_secs(1)))
1466 );
1467 assert_ne!(
1471 etag_for(10, Some(base)),
1472 etag_for(10, Some(base + std::time::Duration::from_micros(1)))
1473 );
1474 }
1475
1476 #[test]
1477 fn etag_strength_rules() {
1478 assert!(!is_weak_etag("\"abc\""));
1479 assert!(is_weak_etag("W/\"abc\""));
1480 assert_eq!(etag_value("W/\"abc\""), "\"abc\"");
1481 assert_eq!(etag_value("\"abc\""), "\"abc\"");
1482 }
1483
1484 #[test]
1485 fn parses_query_strings() {
1486 let params = parse_query_string("a=1&b=hello+world&c=%2Ftmp");
1487 assert_eq!(params.get("a").map(String::as_str), Some("1"));
1488 assert_eq!(params.get("b").map(String::as_str), Some("hello world"));
1489 assert_eq!(params.get("c").map(String::as_str), Some("/tmp"));
1490 }
1491}