1use super::*;
2
3use didbot_http::PrivateAddresses;
4
5#[derive(Debug, serde::Deserialize)]
7pub(crate) struct ResolveHandleQuery {
8 handle: String,
9}
10
11fn 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
20pub(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
116fn 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#[derive(Debug, serde::Deserialize)]
129pub(crate) struct ResolveDidQuery {
130 did: String,
131}
132
133fn 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
142pub(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}