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