1use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7
8use didbot_http::{AddressError, PrivateAddresses};
9use didbot_identity::DidDocument;
10use serde::Deserialize;
11use serde_json::Value;
12use url::Url;
13
14use crate::record::RecordRef;
15
16pub const MAX_RECORD_BYTES: usize = 64 * 1024;
26
27const RECORD_NOT_FOUND: &str = "RecordNotFound";
31
32pub type FetchFuture<'a> = Pin<Box<dyn Future<Output = Fetched> + Send + 'a>>;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Unknown {
45 NotFetched,
49 Deadline,
51 Refused(String),
53 Unresolvable(String),
55 Unreachable(String),
58}
59
60impl Unknown {
61 #[must_use]
63 pub fn as_str(&self) -> &'static str {
64 match self {
65 Unknown::NotFetched => "not-fetched",
66 Unknown::Deadline => "deadline",
67 Unknown::Refused(_) => "refused",
68 Unknown::Unresolvable(_) => "unresolvable",
69 Unknown::Unreachable(_) => "unreachable",
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum Fetched {
77 Present(Value),
83 Absent,
86 Unknown(Unknown),
88}
89
90pub trait Fetch: Send + Sync {
97 fn fetch<'a>(&'a self, record: &'a RecordRef) -> FetchFuture<'a>;
99}
100
101pub trait LocalRepositories: Send + Sync {
110 fn holds(&self, did: &str) -> bool;
112 fn serves(&self, host: &str) -> bool;
114}
115
116#[derive(Debug, Clone, Copy, Default)]
119pub struct NoLocalRepositories;
120
121impl LocalRepositories for NoLocalRepositories {
122 fn holds(&self, _did: &str) -> bool {
123 false
124 }
125 fn serves(&self, _host: &str) -> bool {
126 false
127 }
128}
129
130#[derive(Debug, Deserialize)]
132struct GetRecordResponse {
133 value: Value,
134}
135
136#[derive(Debug, Deserialize)]
138struct ListRecordsResponse {
139 records: Vec<ListedRecord>,
140 #[serde(default)]
141 cursor: Option<String>,
142}
143
144#[derive(Debug, Deserialize)]
146struct ListedRecord {
147 uri: String,
148 value: Value,
149}
150
151pub const MAX_COLLECTION_RECORDS: usize = 100;
162
163pub struct Network {
173 private: PrivateAddresses,
174 local: Arc<dyn LocalRepositories>,
175}
176
177impl Network {
178 #[must_use]
181 pub fn new(local: Arc<dyn LocalRepositories>, private: PrivateAddresses) -> Self {
182 Self { private, local }
183 }
184
185 async fn endpoint(&self, did: &str) -> Result<String, Unknown> {
187 let document_url = didbot_identity::document_url(did)
188 .map_err(|error| Unknown::Unresolvable(format!("{did}: {error}")))?;
189 let body = self
190 .get_bounded(&document_url, didbot_http::MAX_JSON_BODY)
191 .await?;
192 let document: DidDocument = serde_json::from_slice(&body)
193 .map_err(|error| Unknown::Unreachable(format!("parsing {document_url}: {error}")))?;
194 if document.id != did {
199 return Err(Unknown::Unreachable(format!(
200 "the did document at {document_url} claims {:?}, expected {did:?}",
201 document.id
202 )));
203 }
204 let endpoint = document.pds_endpoint().ok_or_else(|| {
205 Unknown::Unreachable(format!(
206 "{did}'s did document names no #atproto_pds service"
207 ))
208 })?;
209 Ok(endpoint.to_owned())
210 }
211
212 async fn get_bounded(&self, url: &str, max_bytes: usize) -> Result<Vec<u8>, Unknown> {
215 let response = self.send(url).await?;
216 let response = response
217 .error_for_status()
218 .map_err(|error| Unknown::Unreachable(format!("{url} answered: {error}")))?;
219 didbot_http::read_bounded(response, max_bytes)
220 .await
221 .map_err(|error| Unknown::Unreachable(format!("reading {url}: {error}")))
222 }
223
224 async fn send(&self, url: &str) -> Result<reqwest::Response, Unknown> {
227 let parsed =
228 Url::parse(url).map_err(|_| Unknown::Unreachable(format!("{url:?} is not a URL")))?;
229 if let Some(host) = parsed.host_str() {
230 if self.local.serves(host) {
231 return Err(Unknown::Refused(format!(
232 "{host} is this deployment; a repository it holds is read from its own store"
233 )));
234 }
235 }
236 let client = didbot_http::pinned_client(&parsed, self.private)
237 .await
238 .map_err(|error| match error {
239 AddressError::Refused { .. } => Unknown::Refused(error.to_string()),
240 other => Unknown::Unreachable(other.to_string()),
241 })?;
242 client
243 .get(parsed)
244 .send()
245 .await
246 .map_err(|error| Unknown::Unreachable(format!("fetching {url}: {error}")))
247 }
248
249 async fn list_records(&self, endpoint: &str, collection: &RecordRef) -> Fetched {
256 let url = format!(
257 "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}&limit={MAX_COLLECTION_RECORDS}",
258 endpoint.trim_end_matches('/'),
259 urlencode(&collection.did),
260 urlencode(&collection.collection),
261 );
262 let body = match self.get_bounded(&url, MAX_RECORD_BYTES).await {
263 Ok(body) => body,
264 Err(unknown) => return Fetched::Unknown(unknown),
265 };
266 let listed: ListRecordsResponse = match serde_json::from_slice(&body) {
267 Ok(listed) => listed,
268 Err(error) => {
269 return Fetched::Unknown(Unknown::Unreachable(format!("parsing {url}: {error}")))
270 }
271 };
272 if listed.cursor.is_some() && listed.records.len() >= MAX_COLLECTION_RECORDS {
273 return Fetched::Unknown(Unknown::Unreachable(format!(
274 "{} holds more than {MAX_COLLECTION_RECORDS} records, which is more of somebody \
275 else's repository than one write reads",
276 collection.at_uri()
277 )));
278 }
279 if listed.records.is_empty() {
280 return Fetched::Absent;
281 }
282 Fetched::Present(Value::Array(
283 listed
284 .records
285 .into_iter()
286 .map(|record| serde_json::json!({ "uri": record.uri, "value": record.value }))
287 .collect(),
288 ))
289 }
290
291 async fn get_record(&self, endpoint: &str, record: &RecordRef, rkey: &str) -> Fetched {
295 let url = format!(
296 "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection={}&rkey={}",
297 endpoint.trim_end_matches('/'),
298 urlencode(&record.did),
299 urlencode(&record.collection),
300 urlencode(rkey),
301 );
302 let response = match self.send(&url).await {
303 Ok(response) => response,
304 Err(unknown) => return Fetched::Unknown(unknown),
305 };
306 let status = response.status().as_u16();
314 if status == 404 {
315 return Fetched::Absent;
316 }
317 if status == 400 {
318 let body = didbot_http::read_bounded(response, MAX_RECORD_BYTES)
321 .await
322 .unwrap_or_default();
323 let named = serde_json::from_slice::<Value>(&body)
324 .ok()
325 .and_then(|value| {
326 value
327 .get("error")
328 .and_then(Value::as_str)
329 .map(str::to_owned)
330 });
331 return match named.as_deref() {
332 Some(RECORD_NOT_FOUND) => Fetched::Absent,
333 Some(other) => Fetched::Unknown(Unknown::Unreachable(format!(
334 "{url} answered 400 {other}, which is not {RECORD_NOT_FOUND}"
335 ))),
336 None => Fetched::Unknown(Unknown::Unreachable(format!(
337 "{url} answered 400 with no atproto error name"
338 ))),
339 };
340 }
341 let response = match response.error_for_status() {
342 Ok(response) => response,
343 Err(error) => {
344 return Fetched::Unknown(Unknown::Unreachable(format!("{url} answered: {error}")))
345 }
346 };
347 let body = match didbot_http::read_bounded(response, MAX_RECORD_BYTES).await {
348 Ok(body) => body,
349 Err(error) => {
350 return Fetched::Unknown(Unknown::Unreachable(format!("reading {url}: {error}")))
351 }
352 };
353 match serde_json::from_slice::<GetRecordResponse>(&body) {
354 Ok(body) => Fetched::Present(Value::Array(vec![serde_json::json!({
355 "uri": record.at_uri(),
356 "value": body.value,
357 })])),
358 Err(error) => Fetched::Unknown(Unknown::Unreachable(format!("parsing {url}: {error}"))),
359 }
360 }
361}
362
363impl Fetch for Network {
364 fn fetch<'a>(&'a self, record: &'a RecordRef) -> FetchFuture<'a> {
365 Box::pin(async move {
366 if self.local.holds(&record.did) {
367 return Fetched::Unknown(Unknown::Refused(format!(
368 "{} is hosted here; its records are read from this server's own store",
369 record.did
370 )));
371 }
372 let endpoint = match self.endpoint(&record.did).await {
373 Ok(endpoint) => endpoint,
374 Err(unknown) => return Fetched::Unknown(unknown),
375 };
376 match &record.rkey {
377 Some(rkey) => self.get_record(&endpoint, record, rkey).await,
378 None => self.list_records(&endpoint, record).await,
379 }
380 })
381 }
382}
383
384fn urlencode(value: &str) -> String {
388 let mut encoded = String::with_capacity(value.len());
389 for byte in value.bytes() {
390 match byte {
391 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
392 encoded.push(byte as char);
393 }
394 other => encoded.push_str(&format!("%{other:02X}")),
395 }
396 }
397 encoded
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
408 fn a_query_value_carries_no_syntax_of_its_own() {
409 assert_eq!(urlencode("did:plc:abc"), "did%3Aplc%3Aabc");
410 assert_eq!(urlencode("3k-a.b_c~d"), "3k-a.b_c~d");
411 assert_eq!(urlencode("a&b=c"), "a%26b%3Dc");
412 }
413
414 #[tokio::test]
415 async fn a_repository_this_deployment_holds_is_never_fetched() {
416 struct Ours;
417 impl LocalRepositories for Ours {
418 fn holds(&self, did: &str) -> bool {
419 did == "did:web:agent.pds.example"
420 }
421 fn serves(&self, _host: &str) -> bool {
422 false
423 }
424 }
425 let network = Network::new(Arc::new(Ours), PrivateAddresses::Refused);
426 let record = RecordRef::new(
427 "did:web:agent.pds.example",
428 "app.bsky.feed.threadgate",
429 "3kabc",
430 );
431 assert!(matches!(
432 network.fetch(&record).await,
433 Fetched::Unknown(Unknown::Refused(_))
434 ));
435 }
436
437 #[tokio::test]
438 async fn a_did_method_nothing_here_resolves_is_unresolvable_not_unreachable() {
439 let network = Network::new(Arc::new(NoLocalRepositories), PrivateAddresses::Refused);
440 let record = RecordRef::new("did:example:abc", "app.bsky.feed.threadgate", "3kabc");
441 assert!(matches!(
442 network.fetch(&record).await,
443 Fetched::Unknown(Unknown::Unresolvable(_))
444 ));
445 }
446
447 #[tokio::test]
451 async fn this_deployments_own_name_is_refused() {
452 struct Us;
453 impl LocalRepositories for Us {
454 fn holds(&self, _did: &str) -> bool {
455 false
456 }
457 fn serves(&self, host: &str) -> bool {
458 host == "pds.example"
459 }
460 }
461 let network = Network::new(Arc::new(Us), PrivateAddresses::Allowed);
462 let record = RecordRef::new("did:web:pds.example", "app.bsky.feed.threadgate", "3kabc");
463 assert!(matches!(
464 network.fetch(&record).await,
465 Fetched::Unknown(Unknown::Refused(_))
466 ));
467 }
468}