Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/chrisvander/crate. The future of cloud storage.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129use crate::error::{ApiError, Result};use crate_protocol::BlobRef;use poem::Body;use sha2::{Digest, Sha256};use tokio::io::AsyncReadExt;
pub async fn read_upload(body: Body, limit: usize) -> Result<Vec<u8>> { let mut bytes = Vec::new(); tokio::time::timeout( std::time::Duration::from_secs(300), body.into_async_read() .take(limit as u64 + 1) .read_to_end(&mut bytes), ) .await .map_err(|_| ApiError::bad("The upload timed out."))? .map_err(|_| ApiError::bad("The upload was interrupted."))?; if bytes.len() > limit { return Err(ApiError::too_large()); } Ok(bytes)}
pub fn mime_type(value: Option<&str>) -> Result<String> { let mime = value .unwrap_or("application/octet-stream") .parse::<mime::Mime>() .map_err(|_| ApiError::bad("Invalid MIME type."))?; Ok(mime.to_string())}
pub fn verify_blob(blob: &BlobRef, bytes: &[u8]) -> Result<()> { BlobDigest::new(bytes).verify(blob)}
pub struct BlobDigest { size: usize, sha256: [u8; 32],}
impl BlobDigest { pub fn new(bytes: &[u8]) -> Self { Self { size: bytes.len(), sha256: Sha256::digest(bytes).into(), } } pub fn verify(&self, blob: &BlobRef) -> Result<()> { let BlobRef::Blob(blob) = blob; let cid = &blob.r#ref.0; if blob.size != self.size || cid.codec() != 0x55 || cid.hash().code() != 0x12 || cid.hash().digest() != self.sha256 { return Err(ApiError::upstream( "File bytes do not match the PDS blob reference.", )); } Ok(()) }}
pub fn disposition(name: &str) -> String { format!( "attachment; filename=\"download\"; filename*=UTF-8''{}", percent_encoding::utf8_percent_encode(name, percent_encoding::NON_ALPHANUMERIC) )}
pub fn byte_range(value: &str, length: usize) -> Option<std::ops::Range<usize>> { let ranges = http_range::HttpRange::parse(value, length as u64).ok()?; if ranges.len() != 1 { return None; } let range = ranges[0]; let end = range.start.checked_add(range.length)?; if range.length == 0 || end > length as u64 { return None; } Some(range.start as usize..end as usize)}
#[cfg(test)]mod tests { use super::*; #[tokio::test] async fn rejects_oversize_without_relying_on_content_length() { assert!(read_upload(Body::from("abcdef"), 5).await.is_err()); assert_eq!(read_upload(Body::from("abc"), 5).await.unwrap(), b"abc"); } #[test] fn attachment_filename_cannot_inject_headers() { let value = disposition("a\r\nContent-Type: text/html"); assert!(!value.contains(['\r', '\n'])); assert!(value.starts_with("attachment;")); }
#[test] fn single_ranges_are_bounded_and_empty_files_never_underflow() { assert_eq!(byte_range("bytes=1-3", 5), Some(1..4)); assert_eq!(byte_range("bytes=-2", 5), Some(3..5)); assert_eq!(byte_range("bytes=2-", 5), Some(2..5)); for value in [ "bytes=0-0", "bytes=-0", "bytes=999999999999999999999-", "bytes=0-1,3-4", ] { assert!(byte_range(value, 0).is_none()); } assert!(byte_range("bytes=5-", 5).is_none()); assert!(byte_range("bytes=0-1,3-4", 5).is_none()); }
#[test] fn verifies_empty_and_nonempty_blob_bytes_and_rejects_tampering() { for bytes in [b"".as_slice(), b"hello".as_slice()] { let cid = cid::Cid::new_v1( 0x55, cid::multihash::Multihash::wrap(0x12, &Sha256::digest(bytes)).unwrap(), ); let blob=serde_json::from_value(serde_json::json!({"$type":"blob","ref":{"$link":cid.to_string()},"mimeType":"text/plain","size":bytes.len()})).unwrap(); assert!(verify_blob(&blob, bytes).is_ok()); assert!(verify_blob(&blob, b"tampered").is_err()); } }}