Skip to main content

didbot_serve/routes/
identity.rs

1use super::*;
2
3use didbot_http::PrivateAddresses;
4
5/// The query `com.atproto.identity.resolveHandle` takes.
6#[derive(Debug, serde::Deserialize)]
7pub(crate) struct ResolveHandleQuery {
8    handle: String,
9}
10
11/// `HandleNotFound`, the one error the lexicon declares.
12fn handle_not_found(handle: &str, why: &str) -> ApiError {
13    ApiError::new(
14        StatusCode::BAD_REQUEST,
15        "HandleNotFound",
16        format!("`{handle}` resolves to no DID: {why}"),
17    )
18}
19
20/// `GET /xrpc/com.atproto.identity.resolveHandle?handle=`
21///
22/// The DID a handle points at. A handle an account here holds answers what
23/// `/.well-known/atproto-did` answers on that hostname, so this route is as
24/// public as that one. Any other handle under this deployment's own domains
25/// is `HandleNotFound` at once.
26///
27/// Every other handle is resolved the way the handle spec says: the
28/// `_atproto` TXT record, then `https://<handle>/.well-known/atproto-did`.
29/// That fetch refuses private addresses, is bounded by
30/// [`crate::handles::LOOKUP_TIMEOUT`], and spends one of the caller's
31/// [`HANDLE_LOOKUP_LIMIT`] lookups. A handle under a TLD the spec reserves
32/// is never looked up. A lookup that finds no DID, fails or times out is
33/// `HandleNotFound`. Like the lexicon says, the DID is not checked back
34/// against its document.
35pub(crate) async fn resolve_handle(
36    State(state): State<AppState>,
37    peer: Peer,
38    headers: HeaderMap,
39    query: Result<Query<ResolveHandleQuery>, QueryRejection>,
40) -> Response {
41    match resolve(&state, peer, &headers, query).await {
42        Ok(did) => Json(json!({ "did": did })).into_response(),
43        Err(err) => err.into_response(),
44    }
45}
46
47async fn resolve(
48    state: &AppState,
49    peer: Peer,
50    headers: &HeaderMap,
51    query: Result<Query<ResolveHandleQuery>, QueryRejection>,
52) -> Result<String, ApiError> {
53    require_lifecycle(
54        state,
55        |p| p.serves_reads,
56        "com.atproto.identity.resolveHandle",
57    )?;
58    let Query(query) = query.map_err(|rejection| ApiError::bad_request(rejection.body_text()))?;
59    didbot_identity::validate_handle(&query.handle)
60        .map_err(|err| ApiError::bad_request(format!("`handle`: {err}")))?;
61    let handle = query.handle.to_ascii_lowercase();
62
63    if let Some(did) = state.registry.handle_did(&handle) {
64        return Ok(did);
65    }
66    if is_own_domain(state, &handle) {
67        return Err(handle_not_found(&handle, "no account here holds it"));
68    }
69    if didbot_identity::has_disallowed_tld(&handle) {
70        return Err(handle_not_found(
71            &handle,
72            "its top-level domain is reserved, and never resolves",
73        ));
74    }
75
76    let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
77    if !state.handle_lookup_limiter.check(&key) {
78        info!(caller = %key, "handle lookup refused: too many from this address");
79        return Err(
80            ApiError::rate_limited("too many handle lookups from this address; slow down")
81                .with_header(
82                    header::RETRY_AFTER,
83                    HANDLE_LOOKUP_WINDOW.as_secs().to_string(),
84                ),
85        );
86    }
87    let lookup = state.handles.did_for(&handle, PrivateAddresses::Refused);
88    let found = tokio::time::timeout(crate::handles::LOOKUP_TIMEOUT, lookup)
89        .await
90        .unwrap_or_else(|_| {
91            Err(format!(
92                "no answer within {:?}",
93                crate::handles::LOOKUP_TIMEOUT
94            ))
95        });
96    match found {
97        Ok(Some(did)) if didbot_identity::validate_did(&did).is_ok() => {
98            debug!(handle, did, "resolved a handle hosted elsewhere");
99            Ok(did)
100        }
101        Ok(Some(did)) => Err(handle_not_found(
102            &handle,
103            &format!("it names `{did}`, which is not a DID"),
104        )),
105        Ok(None) => Err(handle_not_found(
106            &handle,
107            "neither DNS nor HTTPS names a DID for it",
108        )),
109        Err(error) => {
110            debug!(handle, %error, "could not resolve a handle hosted elsewhere");
111            Err(handle_not_found(&handle, &error))
112        }
113    }
114}
115
116/// Whether `handle` is under a domain this deployment issues handles in,
117/// where no lookup could find what the registry does not hold.
118fn is_own_domain(state: &AppState, handle: &str) -> bool {
119    state
120        .registry
121        .available_user_domains()
122        .iter()
123        .map(|suffix| suffix.to_ascii_lowercase())
124        .any(|suffix| handle.ends_with(&suffix) || handle == suffix.trim_start_matches('.'))
125}
126
127/// The query `com.atproto.identity.resolveDid` takes.
128#[derive(Debug, serde::Deserialize)]
129pub(crate) struct ResolveDidQuery {
130    did: String,
131}
132
133/// `DidNotFound`, one of the two errors the lexicon declares.
134fn did_not_found(did: &str, why: &str) -> ApiError {
135    ApiError::new(
136        StatusCode::BAD_REQUEST,
137        "DidNotFound",
138        format!("`{did}` has no DID document: {why}"),
139    )
140}
141
142/// `GET /xrpc/com.atproto.identity.resolveDid?did=`
143///
144/// A DID's document. A DID on a name this deployment answers to is answered
145/// from the registry, as `/.well-known/did.json` answers it on that
146/// hostname, so this route is as public as that one. Such a DID that no
147/// account holds is `DidNotFound` with no lookup.
148///
149/// Every other `did:web` and `did:plc` is fetched as its method says. The
150/// fetch refuses private addresses, is bounded by
151/// [`crate::handles::LOOKUP_TIMEOUT`], and spends one of the caller's
152/// [`DID_LOOKUP_LIMIT`] lookups. A `404` is `DidNotFound`, and a `410`,
153/// which the PLC directory answers for a deactivated DID, is
154/// `DidDeactivated`. Like the lexicon says, the handle in the document is
155/// not checked back.
156pub(crate) async fn resolve_did(
157    State(state): State<AppState>,
158    peer: Peer,
159    headers: HeaderMap,
160    query: Result<Query<ResolveDidQuery>, QueryRejection>,
161) -> Response {
162    match did_document(&state, peer, &headers, query).await {
163        Ok(document) => Json(json!({ "didDoc": document })).into_response(),
164        Err(err) => err.into_response(),
165    }
166}
167
168async fn did_document(
169    state: &AppState,
170    peer: Peer,
171    headers: &HeaderMap,
172    query: Result<Query<ResolveDidQuery>, QueryRejection>,
173) -> Result<serde_json::Value, ApiError> {
174    require_lifecycle(state, |p| p.serves_reads, "com.atproto.identity.resolveDid")?;
175    let Query(query) = query.map_err(|rejection| ApiError::bad_request(rejection.body_text()))?;
176    let did = query.did;
177    didbot_identity::validate_did(&did)
178        .map_err(|err| ApiError::bad_request(format!("`did`: {err}")))?;
179    let url = didbot_identity::document_url(&did)
180        .map_err(|err| ApiError::bad_request(format!("`did`: {err}")))?;
181    let url =
182        url::Url::parse(&url).map_err(|err| ApiError::bad_request(format!("`did`: {err}")))?;
183
184    let host = url.host_str().unwrap_or_default();
185    if crate::foreign::serves(state.registry.as_ref(), host) {
186        let document = state
187            .registry
188            .did_document(host)
189            .filter(|document| document.id == did)
190            .ok_or_else(|| did_not_found(&did, "no account here holds it"))?;
191        return serde_json::to_value(document).map_err(|error| {
192            ApiError::new(
193                StatusCode::INTERNAL_SERVER_ERROR,
194                "DidDocumentUnavailable",
195                error.to_string(),
196            )
197        });
198    }
199
200    let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
201    if !state.did_lookup_limiter.check(&key) {
202        info!(caller = %key, "DID lookup refused: too many from this address");
203        return Err(
204            ApiError::rate_limited("too many DID lookups from this address; slow down")
205                .with_header(header::RETRY_AFTER, DID_LOOKUP_WINDOW.as_secs().to_string()),
206        );
207    }
208    let fetch = crate::foreign::fetch_document(&url, &did, state.tree.private_addresses());
209    let fetched = tokio::time::timeout(crate::handles::LOOKUP_TIMEOUT, fetch)
210        .await
211        .map_err(|_| {
212            debug!(did, "a DID lookup timed out");
213            ApiError::new(
214                StatusCode::GATEWAY_TIMEOUT,
215                "UpstreamTimeout",
216                format!(
217                    "{url} gave no answer within {:?}",
218                    crate::handles::LOOKUP_TIMEOUT
219                ),
220            )
221        })?;
222    match fetched {
223        Ok(crate::foreign::Document::Found(document)) => {
224            debug!(did, "resolved a DID hosted elsewhere");
225            Ok(document)
226        }
227        Ok(crate::foreign::Document::NotFound) => {
228            Err(did_not_found(&did, &format!("{url} holds none")))
229        }
230        Ok(crate::foreign::Document::Gone) => Err(ApiError::new(
231            StatusCode::BAD_REQUEST,
232            "DidDeactivated",
233            format!("`{did}` was deactivated: {url} answered 410"),
234        )),
235        Err(crate::foreign::DocumentError::Address(error)) => {
236            debug!(did, %error, "refused a DID lookup's address");
237            Err(ApiError::bad_request(format!(
238                "`{did}` resolves to an address this server does not reach: {error}"
239            )))
240        }
241        Err(crate::foreign::DocumentError::Failed(reason)) => {
242            debug!(did, reason, "could not resolve a DID hosted elsewhere");
243            Err(ApiError::new(
244                StatusCode::BAD_GATEWAY,
245                "UpstreamFailure",
246                format!("could not resolve `{did}`: {reason}"),
247            ))
248        }
249    }
250}