···3737 /// The response content length exceeded the size limit.
3838 #[error("content exceeded the maximum size")]
3939 TooLarge,
4040- /// An internal client error occured whilst processing the request,
4040+ /// An internal client error occurred whilst processing the request,
4141 /// see [`reqwest::Error`].
4242- #[error("an internal client error occured: {0}")]
4242+ #[error(transparent)]
4343 ClientError(#[from] reqwest::Error),
4444}
4545
+22-18
src/main.rs
···22mod http;
33mod mime;
44mod routes;
55+mod types;
5667use crate::{
78 cache::{CacheBuildOptions, Caches, build_caches},
···910 routes::{delete_cache_handler, get_blob_handler, get_index_handler},
1011};
1112use ::mime::Mime;
1212-#[cfg(not(unix))]
1313-use anyhow::bail;
1413use anyhow::{Context, Result, bail};
1514use axum::{
1615 Router,
···143142 long = "blob-max-size",
144143 env = "PORXIE_BLOB_MAX_SIZE",
145144 default_value = "50mb",
146146- value_parser = |v: &str| -> Result<ByteSize, String> {
145145+ value_parser = |v: &str| -> Result<NonZeroU64, String> {
147146 let size: ByteSize = v.parse().map_err(|e| format!("{e}"))?;
148147 if size.as_u64() < 512_000 {
149148 return Err("minimum allowed value is 512kb".to_string())
···159158 ));
160159 }
161160162162- Ok(size)
161161+ Ok(size.as_u64().try_into().map_err(|e| format!("invalid value {v}: {e}"))?)
163162 }
164163 )]
165165- max_size: ByteSize,
164164+ max_size: NonZeroU64,
166165167166 /// The Cache-Control header value to send alongside blob responses.
168167 ///
···259258 long = "cache-allocation",
260259 env = "PORXIE_CACHE_ALLOCATION",
261260 default_value = "512mb",
262262- value_parser = |v: &str| -> Result<ByteSize, String> {
261261+ value_parser = |v: &str| -> Result<NonZeroU64, String> {
263262 let size: ByteSize = v.parse().map_err(|e| format!("{e}"))?;
264263 if size.as_u64() < 8_000_000 {
265264 return Err("minimum allowed value is 8mb".to_string())
···275274 ));
276275 }
277276278278- Ok(size)
277277+ Ok(size.as_u64().try_into().map_err(|e| format!("invalid value {v}: {e}"))?)
279278 }
280279 )]
281281- size: ByteSize,
280280+ size: NonZeroU64,
282281283282 /// How long blobs can be idle in the cache before expiring.
284283 #[arg(
···306305 default_value = "1h"
307306 )]
308307 policy_ttl: humantime::Duration,
308308+309309+ /// How long identity lookups (DID resolution, etc) can be cached before expiring.
310310+ #[arg(
311311+ id = "CA_CACHE_IDENTITY_TTL",
312312+ long = "cache-identity-ttl",
313313+ env = "PORXIE_CACHE_IDENTITY_TTL",
314314+ default_value = "1h"
315315+ )]
316316+ identity_ttl: humantime::Duration,
309317}
310318311319#[derive(Args)]
···337345 requires = "PA_POLICY_URL",
338346 value_parser = |v: &str| -> Result<(HeaderName, HeaderValue), String> {
339347 let (name, value) = v.split_once(':')
340340- .ok_or_else(|| format!("invalid header {v:?}: expected 'Name: value'"))?;
348348+ .ok_or_else(|| format!("invalid header {v}: expected 'Name: value'"))?;
341349 let name = HeaderName::try_from(name.trim())
342342- .map_err(|e| format!("invalid header name in {v:?}: {e}"))?;
350350+ .map_err(|e| format!("invalid header name in {v}: {e}"))?;
343351 let mut value = HeaderValue::try_from(value.trim())
344344- .map_err(|e| format!("invalid header value in {v:?}: {e}"))?;
352352+ .map_err(|e| format!("invalid header value in {v}: {e}"))?;
345353 value.set_sensitive(true);
346354 Ok((name, value))
347355 }
···439447 )
440448 .context("failed to build policy http client")?,
441449 cache: build_caches(&CacheBuildOptions {
442442- memory_capacity: args.cache.size.as_u64(),
450450+ memory_capacity: args.cache.size,
443451 blob_content_ttl: args.cache.blob_tti.into(),
444452 blob_ownership_ttl: args.cache.ownership_ttl.into(),
445453 blob_policy_ttl: args.cache.policy_ttl.into(),
454454+ identity_cache_ttl: args.cache.identity_ttl.into(),
446455 })
447456 .context("failed to build caches")?,
448457 auth_token: args.server.auth_token,
449458 allowed_mimetypes: args.blob.allowed_mimetypes,
450450- max_blob_size: args
451451- .blob
452452- .max_size
453453- .as_u64()
454454- .try_into()
455455- .context("max blob size was not a non-zero value")?,
459459+ max_blob_size: args.blob.max_size,
456460 cache_control_header: args.blob.cache_header,
457461 policy_service_url: args.policy.url,
458462 policy_service_headers: args.policy.request_headers,
+9-22
src/mime.rs
···11use mime::Mime;
2233/// Sniff the MIME type from the given bytes, returning `application/octet-stream` if unknown.
44+#[must_use]
45pub fn sniff_mime(buf: &[u8]) -> Mime {
56 // WORKAROUND: infer does not correctly detect SVG.
67 // I have created PR to fix this at https://github.com/bojand/infer/pull/119
···2829 }
2930}
30313232+#[must_use]
3133pub fn is_mime_allowed(mime: &Mime, allowed: &[Mime]) -> bool {
3234 const STAR: &str = "*";
3335···6466 fn test_is_mime_allowed() {
6567 // Test PNG when nothing is allowed.
6668 assert_eq!(
6767- super::is_mime_allowed(&Mime::from_str("image/png").unwrap(), &vec![]),
6969+ super::is_mime_allowed(&Mime::from_str("image/png").unwrap(), &[]),
6870 false
6971 );
70727173 // Test PNG when PNG is allowed.
7274 assert_eq!(
7373- super::is_mime_allowed(
7474- &Mime::from_str("image/png").unwrap(),
7575- &vec![mime::IMAGE_PNG],
7676- ),
7575+ super::is_mime_allowed(&Mime::from_str("image/png").unwrap(), &[mime::IMAGE_PNG],),
7776 true
7877 );
79788079 // Test PNG when only JPG is allowed.
8180 assert_eq!(
8282- super::is_mime_allowed(
8383- &Mime::from_str("image/png").unwrap(),
8484- &vec![mime::IMAGE_JPEG],
8585- ),
8181+ super::is_mime_allowed(&Mime::from_str("image/png").unwrap(), &[mime::IMAGE_JPEG],),
8682 false
8783 );
88848985 // Test PNG when any image subtype is allowed.
9086 assert_eq!(
9191- super::is_mime_allowed(
9292- &Mime::from_str("image/png").unwrap(),
9393- &vec![mime::IMAGE_STAR],
9494- ),
8787+ super::is_mime_allowed(&Mime::from_str("image/png").unwrap(), &[mime::IMAGE_STAR],),
9588 true
9689 );
97909891 // Test PNG when anything is allowed.
9992 assert_eq!(
100100- super::is_mime_allowed(
101101- &Mime::from_str("image/png").unwrap(),
102102- &vec![mime::STAR_STAR],
103103- ),
9393+ super::is_mime_allowed(&Mime::from_str("image/png").unwrap(), &[mime::STAR_STAR],),
10494 true
10595 );
1069610797 // Test HTML when any image subtype is enabled.
10898 assert_eq!(
109109- super::is_mime_allowed(
110110- &Mime::from_str("text/html").unwrap(),
111111- &vec![mime::IMAGE_STAR],
112112- ),
9999+ super::is_mime_allowed(&Mime::from_str("text/html").unwrap(), &[mime::IMAGE_STAR],),
113100 false
114101 );
115102···117104 assert_eq!(
118105 super::is_mime_allowed(
119106 &Mime::from_str("image/png").unwrap(),
120120- &vec![mime::TEXT_STAR, mime::IMAGE_STAR],
107107+ &[mime::TEXT_STAR, mime::IMAGE_STAR],
121108 ),
122109 true
123110 );
+48-32
src/routes/blob/get.rs
···11use crate::http::{BytesStreamCappedError, bytes_stream_capped};
22use crate::routes::ErrorResponse;
33+use crate::types::blob_cid::BlobCid;
34use crate::{
45 AppState,
56 cache::{CachedBlobData, CachedBlobPolicy},
···1314};
1415use cid::Cid;
1516use jacquard_common::types::did::Did;
1616-use jacquard_identity::resolver::{IdentityError, IdentityResolver};
1717+use jacquard_identity::resolver::IdentityResolver;
1718use multihash_codetable::{Code, MultihashDigest};
1819use reqwest::Url;
1920use std::sync::Arc;
20212122enum BlobPolicyError {
2222- /// The policy service returned an unexpected status code,.
2323+ /// The policy service returned an unexpected status code.
2324 UnhandledStatusCode,
2425 /// The request to the policy service failed, for example due to the server being unavailable.
2526 FetchFailed,
···6263 FetchFailure,
6364}
64656666+/// Create a `/xrpc/com.atproto.sync.getBlob` Url for the DID+CID.
6767+#[inline]
6868+#[must_use]
6969+fn to_pds_blob_url(mut pds_url: Url, did: &Did<'_>, cid: &BlobCid) -> Url {
7070+ pds_url.set_path("/xrpc/com.atproto.sync.getBlob");
7171+ pds_url
7272+ .query_pairs_mut()
7373+ .append_pair("did", did.as_str())
7474+ .append_pair("cid", &cid.to_string());
7575+ pds_url
7676+}
7777+6578pub async fn get_blob_handler(
6679 Path((raw_did, raw_cid)): Path<(String, String)>,
6780 State(state): State<Arc<AppState>>,
6881) -> Result<axum::response::Response, (StatusCode, Json<ErrorResponse>)> {
6969- /// Resolve the given DID to a PDS URL and then build the `/xrpc/com.atproto.sync.getBlob` url for the DID+CID.
7070- #[inline]
7171- async fn get_blob_url(
7272- state: &AppState,
7373- did: &Did<'_>,
7474- cid: &Cid,
7575- ) -> Result<Url, IdentityError> {
7676- let mut url = state.identity_resolver.pds_for_did(did).await?;
7777- url.set_path("/xrpc/com.atproto.sync.getBlob");
7878- url.query_pairs_mut()
7979- .append_pair("did", did.as_str())
8080- .append_pair("cid", &cid.to_string());
8181- Ok(url)
8282- }
8383-8482 let (did, cid) = (
8583 match Did::new_owned(raw_did.as_str()) {
8684 Ok(did) => did,
···9492 ));
9593 }
9694 },
9797- match Cid::try_from(raw_cid.as_str()) {
9595+ match BlobCid::try_from(raw_cid.as_str()) {
9896 Ok(cid) => cid,
9997 Err(_) => {
10098 return Err((
···108106 },
109107 );
110108111111- // Check policy for this DID+CID; concurrent requests for a key are coalesced.
109109+ // Check policy for this DID+CID; concurrent requests for the same key are coalesced.
112110 if let Some(ref policy_service_url) = state.policy_service_url {
113111 match state
114112 .cache
···186184 .blob_content
187185 .try_get_with_by_ref(&cid, async {
188186 tracing::debug!("fetching blob from PDS");
189189- let blob_url = get_blob_url(&state, &did, &cid).await.map_err(|err| {
190190- tracing::debug!("failed to resolve PDS: {err:?}");
191191- BlobDownloadError::DidPdsResolutionFailure
192192- })?;
187187+ let blob_url = to_pds_blob_url(
188188+ state
189189+ .cache
190190+ .identity
191191+ .try_get_with_by_ref(&did, state.identity_resolver.pds_for_did(&did))
192192+ .await
193193+ .map_err(|err| {
194194+ tracing::debug!("failed to resolve PDS: {:?}", *err);
195195+ BlobDownloadError::DidPdsResolutionFailure
196196+ })?,
197197+ &did,
198198+ &cid,
199199+ );
193200194201 let validated_bytes = {
195202 let response = state
···204211205212 // Gracefully handle & abort if we do not receive a successful status code.
206213 if !response.status().is_success() {
207207- // Note: Bluesky's PDS implemenation sends 400 instead of 404 when a blob is
214214+ // Note: Bluesky's PDS implementation sends 400 instead of 404 when a blob is
208215 // not found. This will skip the 404 handler and instead count as an error.
209216 // This is not our responsibility to work around as other implementations do it right.
210217 return Err(match response.status() {
···253260 }
254261 }?;
255262256256- if computed_cid != cid {
263263+ if computed_cid != *cid {
257264 tracing::warn!("cid mismatch: computed {computed_cid} expected {cid}");
258265 return Err(BlobDownloadError::CidMismatch);
259266 }
···298305 const { HeaderValue::from_static("attachment") },
299306 );
300307301301- // Mark this key as verified in the the ownership cache.
308308+ // Mark this key as verified in the ownership cache.
302309 state
303310 .cache
304311 .blob_ownership
···377384 .blob_ownership
378385 .try_get_with((cid, did.clone()), async {
379386 tracing::debug!("verifying ownership of blob");
380380- let blob_url = get_blob_url(&state, &did, &cid).await.map_err(|err| {
381381- tracing::debug!("failed to resolve PDS url: {err:?}");
382382- BlobOwnershipError::DidPdsResolutionFailure
383383- })?;
387387+ let blob_url = to_pds_blob_url(
388388+ state
389389+ .cache
390390+ .identity
391391+ .try_get_with_by_ref(&did, state.identity_resolver.pds_for_did(&did))
392392+ .await
393393+ .map_err(|err| {
394394+ tracing::debug!("failed to resolve PDS: {:?}", *err);
395395+ BlobOwnershipError::DidPdsResolutionFailure
396396+ })?,
397397+ &did,
398398+ &cid,
399399+ );
384400385401 // Request the blob with as little of the actual body as we can.
386402 //
387403 // While some PDS implementations (bsky, tranquil) support HTTP HEAD, it is not
388388- // actually apart of the XRPC specification and we cannot rely on it (for now).
404404+ // actually a part of the XRPC specification and we cannot rely on it (for now).
389405 // Use a range request to avoid downloading the full body on servers that support it instead.
390406 match state
391407 .blob_fetch_http_client
392408 .get(blob_url)
393409 .header(
394410 header::RANGE,
395395- const { HeaderValue::from_static("bytes=0-1") },
411411+ const { HeaderValue::from_static("bytes=0-1023") },
396412 )
397413 .send()
398414 .await