1use std::collections::HashSet;
2use std::fmt::{self, Display};
3use std::sync::{Arc, Mutex, OnceLock};
4use std::time::Duration;
5
6use bytes::Bytes;
7use http::request::Parts;
8use http::{Method, StatusCode};
9use hyper::body::{Body, Incoming};
10use serde::Deserialize;
11use serde_json::{Map, Value};
12use sofie_auth::SigningKeys;
13use sofie_core::{Error, Result, unpoisoned};
14use sofie_storage::{ContentType, Put, Storage, StorageKey};
15use tokio::time::Instant;
16
17use crate::Serving;
18use crate::answer::{self, Response};
19use crate::caller::Caller;
20use crate::deadline::{Deadline, Paused};
21use crate::multipart::{self, Multipart, PartHead, Stop};
22use crate::pipeline::{Asked, RequestBody, discard_rest, read_body, too_large};
23use crate::routes::BODY_LIMIT;
24use crate::target::Target;
25use crate::urlencoded::Encoding;
26
27const TEXT_LIMIT: usize = BODY_LIMIT;
30const PART_LIMIT: usize = 100;
31const FILENAME_LIMIT: usize = 255;
32const SEAL: &str = "seal";
33const SEAL_LIFETIME: Duration = Duration::from_mins(2);
35const FORM_TIME: Duration = Duration::from_mins(10);
37
38tokio::task_local! {
39 static TAKEN: Arc<Mutex<Taken>>;
41}
42
43#[derive(Default)]
44struct Taken {
45 keys: HashSet<StorageKey>,
46 committed: bool,
47}
48
49pub(crate) fn committed() {
51 let _outside_a_request = TAKEN.try_with(|taken| unpoisoned::lock(taken).committed = true);
53}
54
55#[derive(Clone, PartialEq, Eq)]
58pub struct Upload {
59 key: StorageKey,
60 filename: String,
61 content_type: ContentType,
62 size: u64,
63}
64
65impl Upload {
66 #[must_use]
67 pub fn key(&self) -> &StorageKey {
68 &self.key
69 }
70
71 #[must_use]
74 pub fn filename(&self) -> &str {
75 &self.filename
76 }
77
78 #[must_use]
80 pub fn content_type(&self) -> &ContentType {
81 &self.content_type
82 }
83
84 #[must_use]
85 pub fn size(&self) -> u64 {
86 self.size
87 }
88
89 fn sealed(&self, expires: u64) -> String {
90 format!(
91 "{}\n{}\n{}\n{}\n{expires}",
92 self.key, self.content_type, self.size, self.filename
93 )
94 }
95
96 fn to_json(&self) -> Result<Value> {
97 self.sealed_json(seconds_now() + SEAL_LIFETIME.as_secs())
98 }
99
100 fn sealed_json(&self, expires: u64) -> Result<Value> {
101 let sealed = self.sealed(expires);
102 let seal = seal_keys()?.sign(sofie_auth::Signed::UploadSeal, sealed.as_bytes());
103 Ok(serde_json::json!({
104 "key": self.key,
105 "filename": self.filename,
106 "content_type": self.content_type,
107 "size": self.size,
108 "expires": expires,
109 SEAL: seal,
110 }))
111 }
112}
113
114impl fmt::Debug for Upload {
115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116 formatter
117 .debug_struct("Upload")
118 .field("key", &self.key)
119 .field("content_type", &self.content_type)
120 .field("size", &self.size)
121 .finish_non_exhaustive()
122 }
123}
124
125#[derive(Deserialize)]
126#[serde(deny_unknown_fields)]
127struct Received {
128 key: StorageKey,
129 filename: String,
130 content_type: ContentType,
131 size: u64,
132 expires: u64,
133 seal: String,
134}
135
136impl<'de> Deserialize<'de> for Upload {
137 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
138 let not_received = || serde::de::Error::custom("is not a file sent with this form");
139 let received = Received::deserialize(deserializer).map_err(|_| not_received())?;
140 let upload = Self {
141 key: received.key,
142 filename: received.filename,
143 content_type: received.content_type,
144 size: received.size,
145 };
146 let keys = seal_keys().map_err(Error::into_deserialize_error)?;
147 let sealed = upload.sealed(received.expires);
148 if received.expires <= seconds_now()
149 || !keys.verifies(
150 sofie_auth::Signed::UploadSeal,
151 sealed.as_bytes(),
152 &received.seal,
153 )
154 {
155 return Err(not_received());
156 }
157 let _outside_a_request = TAKEN.try_with(|taken| {
159 unpoisoned::lock(taken).keys.insert(upload.key.clone());
160 });
161 Ok(upload)
162 }
163}
164
165#[cfg(feature = "engine")]
166impl sofie_values::Present for Upload {
167 const INPUT: sofie_values::Input = sofie_values::Input::new(sofie_values::Control::File);
168 const SEARCH: sofie_values::Search = sofie_values::Search::Skipped;
169
170 fn csv(&self) -> String {
171 self.filename.clone()
172 }
173}
174
175pub(crate) fn is_upload(value: &Value) -> bool {
176 value.get(SEAL).is_some()
177}
178
179fn seconds_now() -> u64 {
180 std::time::SystemTime::now()
181 .duration_since(std::time::UNIX_EPOCH)
182 .map_or(0, |since| since.as_secs())
183}
184
185fn seal_keys() -> Result<&'static SigningKeys> {
187 static KEYS: OnceLock<SigningKeys> = OnceLock::new();
188 if let Some(keys) = KEYS.get() {
189 return Ok(keys);
190 }
191 let generated = SigningKeys::generate()?;
192 Ok(KEYS.get_or_init(|| generated))
193}
194
195#[derive(Default)]
199pub(crate) struct Stored {
200 storage: Option<Storage>,
201 keys: Vec<StorageKey>,
202 taken: Arc<Mutex<Taken>>,
203}
204
205impl Stored {
206 pub(crate) async fn taking<F: Future>(&self, handler: F) -> F::Output {
207 TAKEN.scope(Arc::clone(&self.taken), handler).await
208 }
209
210 pub(crate) async fn settle(mut self) {
211 let mut keys = std::mem::take(&mut self.keys);
212 {
213 let taken = unpoisoned::lock(&self.taken);
214 if taken.committed {
215 keys.retain(|key| !taken.keys.contains(key));
216 }
217 }
218 if let Some(storage) = &self.storage {
219 discard(storage, &keys).await;
220 }
221 }
222}
223
224impl Drop for Stored {
225 fn drop(&mut self) {
226 let keys = std::mem::take(&mut self.keys);
227 if let Some(storage) = self.storage.take()
228 && !keys.is_empty()
229 {
230 tokio::spawn(async move { discard(&storage, &keys).await });
231 }
232 }
233}
234
235async fn discard(storage: &Storage, keys: &[StorageKey]) {
236 for key in keys {
237 if let Err(error) = storage.delete(key).await {
238 tracing::warn!(error.kind = "storage", error = %error, "deleting an unused upload failed");
239 }
240 }
241}
242
243pub(crate) async fn read(
248 asked: Asked<'_>,
249 target: &Target,
250 serving: &Serving,
251 mut body: Incoming,
252 limit: usize,
253) -> Result<RequestBody, Box<Response>> {
254 let (request, caller) = (asked.request, asked.caller);
255 let boundary = multipart::boundary(&request.headers).filter(|_| !is_webhook(target));
256 let Some(boundary) = boundary else {
257 let limit = match target.admission.rules.file_limit {
258 Some(_) => limit.min(TEXT_LIMIT),
259 None => limit,
260 };
261 let body = read_body(&mut body, limit, caller).await?;
262 return Ok(RequestBody::plain(body, &request.headers));
263 };
264 let refused = |error: Error| Box::new(answer::failure(error, caller));
265 let boundary = match boundary {
266 Ok(boundary) => boundary,
267 Err(stop) => return Err(refused_unread(body, malformed(&stop), caller).await),
268 };
269 let storage = serving
270 .storage
271 .as_ref()
272 .filter(|_| takes_files(request, target));
273 let rules = &target.admission.rules;
274 let file_limit = rules.file_limit.unwrap_or(limit);
275 let limit = match (rules.body_limit, rules.file_limit) {
276 (None, Some(file_limit)) => file_limit.saturating_add(TEXT_LIMIT),
277 _ => limit,
278 };
279 let mut form = Form::new(body, &boundary, limit, storage, file_limit, asked.deadline);
280 let read = form.read().await;
281 match read {
282 Ok(()) => {
283 let encoded = |fields: &Map<String, Value>| {
284 serde_json::to_vec(fields)
285 .map(Bytes::from)
286 .map_err(|error| refused(Error::internal("encoding a form's fields", error)))
287 };
288 Ok(RequestBody {
289 body: encoded(&form.fields)?,
290 #[cfg(feature = "idempotency")]
291 fingerprint: encoded(&as_sent(&form.fields))?,
292 encoding: Encoding::Json,
293 stored: form.stored,
294 })
295 }
296 Err(failure) => {
297 if matches!(failure, Failure::TakesNoFiles)
298 && serving.storage.is_none()
299 && target.admission.rules.file_limit.is_some()
300 {
301 tracing::warn!(
302 error.kind = "storage",
303 "a form sent a file, but the app has no storage; see `Serving::storage`"
304 );
305 }
306 form.stored.settle().await;
307 Err(Box::new(failure.answer(form.multipart, caller).await))
308 }
309 }
310}
311
312#[cfg(feature = "idempotency")]
314fn as_sent(fields: &Map<String, Value>) -> Map<String, Value> {
315 let mut sent = fields.clone();
316 for value in sent.values_mut() {
317 if let Value::Object(upload) = value
318 && upload.contains_key(SEAL)
319 {
320 upload.retain(|name, _| matches!(name.as_str(), "filename" | "content_type" | "size"));
321 }
322 }
323 sent
324}
325
326fn is_webhook(target: &Target) -> bool {
329 #[cfg(feature = "webhooks")]
330 return target.admission.rules.verify.is_some();
331 #[cfg(not(feature = "webhooks"))]
332 {
333 let _ = target;
334 false
335 }
336}
337
338fn takes_files(request: &Parts, target: &Target) -> bool {
339 #[cfg(feature = "jobs")]
340 if target.admission.rules.queues {
341 return false;
342 }
343 target.admission.rules.file_limit.is_some()
344 && request.method != Method::GET
345 && request.method != Method::HEAD
346}
347
348async fn refused_unread(mut body: Incoming, error: Error, caller: Caller<'_>) -> Box<Response> {
349 discard_rest(&mut body).await;
350 Box::new(answer::failure(error, caller))
351}
352
353enum Failure {
354 Stopped(Stop),
355 Refused(Error),
356 TakesNoFiles,
358 TooSlow,
359}
360
361impl From<Stop> for Failure {
362 fn from(stop: Stop) -> Self {
363 Self::Stopped(stop)
364 }
365}
366
367impl From<Error> for Failure {
368 fn from(error: Error) -> Self {
369 Self::Refused(error)
370 }
371}
372
373impl Failure {
374 async fn answer(self, multipart: Multipart<Incoming>, caller: Caller<'_>) -> Response {
376 discard_rest(&mut multipart.into_body()).await;
377 match self {
378 Self::Stopped(Stop::TooLarge) => too_large(caller),
379 Self::Stopped(stop) => answer::failure(malformed(&stop), caller),
380 Self::Refused(error) => answer::failure(error, caller),
381 Self::TakesNoFiles => answer::refusal(
382 StatusCode::UNSUPPORTED_MEDIA_TYPE,
383 "This address does not take files.",
384 caller,
385 ),
386 Self::TooSlow => answer::refusal(
387 StatusCode::REQUEST_TIMEOUT,
388 "The form took too long to arrive.",
389 caller,
390 ),
391 }
392 }
393}
394
395fn malformed(stop: &Stop) -> Error {
396 let reason = match stop {
397 Stop::TooLarge => "it is too large",
398 Stop::Malformed(reason) => reason,
399 Stop::Unread(reason) => reason,
400 };
401 Error::BadRequest(format!("the form cannot be read: {reason}"))
402}
403
404struct Form<'s, B = Incoming> {
405 multipart: Multipart<B>,
406 storage: Option<&'s Storage>,
407 file_limit: usize,
408 fields: Map<String, Value>,
409 stored: Stored,
410 deadline: &'s Deadline,
411 paused: Option<Paused<'s>>,
412 ends: Instant,
413}
414
415impl<'s, B: Body<Data = Bytes> + Unpin> Form<'s, B>
416where
417 B::Error: Display,
418{
419 fn new(
420 body: B,
421 boundary: &str,
422 limit: usize,
423 storage: Option<&'s Storage>,
424 file_limit: usize,
425 deadline: &'s Deadline,
426 ) -> Self {
427 Self {
428 multipart: Multipart::new(body, boundary, limit),
429 storage,
430 file_limit,
431 fields: Map::new(),
432 stored: Stored {
433 storage: storage.cloned(),
434 keys: Vec::new(),
435 taken: Arc::default(),
436 },
437 deadline,
438 paused: None,
439 ends: Instant::now() + FORM_TIME,
440 }
441 }
442
443 async fn read(&mut self) -> Result<(), Failure> {
444 let read = self.parts().await;
445 self.paused = None;
446 read
447 }
448
449 async fn next_part(&mut self) -> Result<Option<PartHead>, Failure> {
450 within(self.ends, self.multipart.next_part()).await
451 }
452
453 async fn next_chunk(&mut self) -> Result<Option<Bytes>, Failure> {
454 within(self.ends, self.multipart.next_chunk()).await
455 }
456
457 async fn parts(&mut self) -> Result<(), Failure> {
458 let mut parts = 0;
459 let mut text = 0;
460 while let Some(head) = self.next_part().await? {
461 parts += 1;
462 if parts > PART_LIMIT {
463 return Err(Error::BadRequest(format!(
464 "the form has more than {PART_LIMIT} fields"
465 ))
466 .into());
467 }
468 match head.filename {
469 Some(_) => self.file(head).await?,
470 None => text += self.text(&head.name, TEXT_LIMIT - text).await?,
471 }
472 }
473 Ok(())
474 }
475
476 async fn text(&mut self, name: &str, room: usize) -> Result<usize, Failure> {
477 let mut value = Vec::new();
478 while let Some(chunk) = self.next_chunk().await? {
479 if value.len() + chunk.len() > room {
480 return Err(Stop::TooLarge.into());
481 }
482 value.extend_from_slice(&chunk);
483 }
484 let length = value.len();
485 let value = String::from_utf8(value)
486 .map_err(|_| Error::BadRequest("the form is not UTF-8".to_owned()))?;
487 if !value.is_empty() {
488 crate::form::insert(&mut self.fields, name, value)?;
489 }
490 Ok(length)
491 }
492
493 async fn file(&mut self, head: PartHead) -> Result<(), Failure> {
494 let name = head.name;
495 if !sofie_core::is_field_name(&name) || self.fields.contains_key(&name) {
496 return Err(Error::BadRequest(format!(
497 "the form's file field `{name}` is not a plain field name or is sent twice"
498 ))
499 .into());
500 }
501 if self.storage.is_some() && self.paused.is_none() {
502 self.paused = Some(self.deadline.paused());
503 }
504 let Some(first) = self.next_chunk().await? else {
505 return Ok(());
506 };
507 let declared = head
508 .content_type
509 .as_deref()
510 .map_or_else(|| Ok(ContentType::octet_stream()), ContentType::parse)
511 .map_err(|_| Error::invalid(&name, "does not declare a media type"))?;
512 let Some(storage) = self.storage else {
513 return Err(Failure::TakesNoFiles);
514 };
515 let mut put = storage.put().await?;
516 let written = self.write(&mut put, first, &declared, &name).await;
517 match written {
518 Ok(()) => {
519 let size = put.size();
520 let key = put.finish().await?;
521 self.stored.keys.push(key.clone());
522 let upload = Upload {
523 key,
524 filename: sanitized(head.filename.as_deref().unwrap_or_default()),
525 content_type: declared,
526 size,
527 };
528 self.fields.insert(name, upload.to_json()?);
529 Ok(())
530 }
531 Err(failure) => {
532 if let Err(error) = put.abort().await {
533 tracing::warn!(error.kind = "storage", error = %error, "discarding a refused upload failed");
534 }
535 Err(failure)
536 }
537 }
538 }
539
540 async fn write(
541 &mut self,
542 put: &mut Put,
543 first: Bytes,
544 declared: &ContentType,
545 name: &str,
546 ) -> Result<(), Failure> {
547 let mut start = Vec::with_capacity(ContentType::SNIFFED_LENGTH);
548 let mut chunk = Some(first);
549 while let Some(data) = chunk {
550 let size = put.size() + u64::try_from(data.len()).unwrap_or(u64::MAX);
551 if size > u64::try_from(self.file_limit).unwrap_or(u64::MAX) {
552 return Err(Error::invalid(
553 name,
554 format!("must be at most {} bytes", self.file_limit),
555 )
556 .into());
557 }
558 let missing = ContentType::SNIFFED_LENGTH.saturating_sub(start.len());
559 start.extend_from_slice(&data[..missing.min(data.len())]);
560 put.write(data).await?;
561 chunk = self.next_chunk().await?;
562 }
563 if !declared.matches(&start) {
564 return Err(
565 Error::invalid(name, format!("is not the {declared} it claims to be")).into(),
566 );
567 }
568 Ok(())
569 }
570}
571
572async fn within<T>(
573 ends: Instant,
574 reading: impl Future<Output = Result<T, Stop>>,
575) -> Result<T, Failure> {
576 let read = tokio::time::timeout_at(ends, reading).await;
577 read.map_err(|_| Failure::TooSlow)?.map_err(Failure::from)
578}
579
580fn sanitized(filename: &str) -> String {
583 let base = filename.rsplit(['/', '\\']).next().unwrap_or_default();
584 let kept: String = base
585 .chars()
586 .filter(|character| {
587 !sofie_core::misleads_display(*character) && !"\"<>:|?*".contains(*character)
588 })
589 .collect();
590 let trimmed =
591 kept.trim_matches(|character: char| character == '.' || character.is_whitespace());
592 let mut end = trimmed.len().min(FILENAME_LIMIT);
593 while !trimmed.is_char_boundary(end) {
594 end -= 1;
595 }
596 match &trimmed[..end] {
597 "" => "file".to_owned(),
598 kept => kept.to_owned(),
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use std::convert::Infallible;
605 use std::pin::Pin;
606 use std::task::{Context, Poll};
607
608 use hyper::body::{Body, Frame};
609 use tokio::sync::mpsc;
610 use tokio::time::sleep;
611
612 use super::*;
613
614 const BOUNDARY: &str = "trickle";
615
616 struct Trickle(mpsc::Receiver<Bytes>);
617
618 impl Body for Trickle {
619 type Data = Bytes;
620 type Error = Infallible;
621
622 fn poll_frame(
623 mut self: Pin<&mut Self>,
624 context: &mut Context<'_>,
625 ) -> Poll<Option<Result<Frame<Bytes>, Infallible>>> {
626 self.0
627 .poll_recv(context)
628 .map(|chunk| chunk.map(|bytes| Ok(Frame::data(bytes))))
629 }
630 }
631
632 fn trickled(head: &'static str, bytes: usize, pause: Duration) -> Trickle {
634 let (sender, receiver) = mpsc::channel(1);
635 tokio::spawn(async move {
636 let opening = format!("--{BOUNDARY}\r\n{head}\r\n\r\n");
637 sender.send(Bytes::from(opening)).await.unwrap();
638 for _ in 0..bytes {
639 sleep(pause).await;
640 let _closed = sender.send(Bytes::from_static(b"x")).await;
641 }
642 let closing = format!("\r\n--{BOUNDARY}--\r\n");
643 let _closed = sender.send(Bytes::from(closing)).await;
644 });
645 Trickle(receiver)
646 }
647
648 async fn read_within_deadline(body: Trickle) -> Option<Result<(), Failure>> {
649 let directory = std::env::temp_dir().join(format!(
650 "sofie-http-trickle-{}",
651 StorageKey::generate().unwrap()
652 ));
653 let keys = Arc::new(SigningKeys::generate().unwrap());
654 let storage = Storage::new(sofie_storage::Local::new(&directory, keys).unwrap());
655 let deadline = Deadline::default();
656 let mut form = Form::new(body, BOUNDARY, 1024, Some(&storage), 1024, &deadline);
657 let read = deadline.within(form.read()).await;
658 drop(form);
659 std::fs::remove_dir_all(&directory).unwrap();
660 read
661 }
662
663 #[tokio::test(start_paused = true)]
664 async fn only_a_file_part_pauses_the_deadline_and_then_for_ten_minutes_at_most() {
665 let text = r#"Content-Disposition: form-data; name="title""#;
666 let file = r#"Content-Disposition: form-data; name="photo"; filename="a.bin""#;
667 let under_the_silence_timeout = Duration::from_secs(50);
668
669 let slow_text = read_within_deadline(trickled(text, 2, under_the_silence_timeout)).await;
670 let slow_file = read_within_deadline(trickled(file, 4, under_the_silence_timeout)).await;
671 let endless_file =
672 read_within_deadline(trickled(file, 15, under_the_silence_timeout)).await;
673
674 assert!(slow_text.is_none());
675 assert!(matches!(slow_file, Some(Ok(()))));
676 assert!(matches!(endless_file, Some(Err(Failure::TooSlow))));
677 }
678
679 #[test]
680 fn a_filename_keeps_only_its_last_safe_segment() {
681 for (sent, kept) in [
682 ("photo.png", "photo.png"),
683 ("C:\\Users\\ada\\photo.png", "photo.png"),
684 ("../../etc/passwd", "passwd"),
685 ("..", "file"),
686 ("", "file"),
687 (" .hidden. ", "hidden"),
688 ("a\"b<c>|?*\u{0}\n.txt", "abc.txt"),
689 ("Jürgen's résumé.pdf", "Jürgen's résumé.pdf"),
690 ] {
691 assert_eq!(sanitized(sent), kept, "{sent:?}");
692 }
693 let long = "é".repeat(200);
694 assert_eq!(sanitized(&long).len(), 254);
695 }
696
697 fn upload() -> Upload {
698 Upload {
699 key: StorageKey::generate().unwrap(),
700 filename: "photo.png".to_owned(),
701 content_type: ContentType::parse("image/png").unwrap(),
702 size: 12,
703 }
704 }
705
706 #[test]
707 fn only_an_upload_this_process_sealed_is_read_back() {
708 let upload = upload();
709 let json = upload.to_json().unwrap();
710
711 assert_eq!(
712 serde_json::from_value::<Upload>(json.clone()).unwrap(),
713 upload
714 );
715 for (field, forged) in [
716 ("key", Value::from(StorageKey::generate().unwrap().as_str())),
717 ("size", Value::from(13)),
718 ("expires", Value::from(u64::MAX)),
719 ("content_type", Value::from("text/html")),
720 ("filename", Value::from("other.png")),
721 (SEAL, Value::from("AAAA")),
722 ] {
723 let mut forged_json = json.clone();
724 forged_json[field] = forged;
725 assert!(
726 serde_json::from_value::<Upload>(forged_json).is_err(),
727 "{field}"
728 );
729 }
730 assert!(serde_json::from_value::<Upload>(Value::from("photo.png")).is_err());
731 let expired = upload.sealed_json(seconds_now() - 1).unwrap();
732 assert!(serde_json::from_value::<Upload>(expired).is_err());
733 }
734}