QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.

refactor: resolve handle view

+200 -92
+3
Cargo.lock
··· 314 dependencies = [ 315 "android-tzdata", 316 "iana-time-zone", 317 "num-traits", 318 "windows-link", 319 ] 320 ··· 1902 "atproto-identity", 1903 "axum", 1904 "bincode", 1905 "deadpool-redis", 1906 "metrohash", 1907 "reqwest",
··· 314 dependencies = [ 315 "android-tzdata", 316 "iana-time-zone", 317 + "js-sys", 318 "num-traits", 319 + "wasm-bindgen", 320 "windows-link", 321 ] 322 ··· 1904 "atproto-identity", 1905 "axum", 1906 "bincode", 1907 + "chrono", 1908 "deadpool-redis", 1909 "metrohash", 1910 "reqwest",
+1
Cargo.toml
··· 19 atproto-identity = { version = "0.11.3" } 20 axum = { version = "0.8" } 21 bincode = { version = "2.0.1", features = ["serde"] } 22 deadpool-redis = { version = "0.22", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] } 23 metrohash = "1.0.7" 24 reqwest = { version = "0.12", features = ["json"] }
··· 19 atproto-identity = { version = "0.11.3" } 20 axum = { version = "0.8" } 21 bincode = { version = "2.0.1", features = ["serde"] } 22 + chrono = "0.4" 23 deadpool-redis = { version = "0.22", features = ["connection-manager", "tokio-comp", "tokio-rustls-comp"] } 24 metrohash = "1.0.7" 25 reqwest = { version = "0.12", features = ["json"] }
+196 -92
src/http/handle_xrpc_resolve_handle.rs
··· 1 use std::sync::Arc; 2 3 use crate::{ 4 handle_resolver::HandleResolver, ··· 34 message: String, 35 } 36 37 /// Calculate a weak ETag for the given content using MetroHash64 with a seed 38 fn calculate_etag(content: &str, seed: &str) -> String { 39 let mut hasher = MetroHash64::new(); ··· 43 format!("W/\"{:x}\"", hash) 44 } 45 46 pub(super) async fn handle_xrpc_resolve_handle( 47 headers: HeaderMap, 48 Query(params): Query<ResolveHandleParams>, 49 State(app_context): State<AppContext>, 50 State(handle_resolver): State<Arc<dyn HandleResolver>>, 51 State(queue): State<Arc<dyn QueueAdapter<HandleResolutionWork>>>, 52 - ) -> Result<Response, Response> { 53 let validating = params.validate.is_some(); 54 let queueing = params.queue.is_some(); 55 ··· 57 let handle = match params.handle { 58 Some(h) => h, 59 None => { 60 - return Err(( 61 StatusCode::BAD_REQUEST, 62 Json(ErrorResponse { 63 error: "InvalidRequest".to_string(), 64 message: "Error: Params must have the property \"handle\"".to_string(), 65 }), 66 ) 67 - .into_response()); 68 } 69 }; 70 ··· 73 Ok(InputType::Handle(value)) => value, 74 Ok(InputType::Plc(_)) | Ok(InputType::Web(_)) => { 75 // It's a DID, not a handle 76 - return Err(( 77 StatusCode::BAD_REQUEST, 78 Json(ErrorResponse { 79 error: "InvalidRequest".to_string(), 80 message: "Error: handle must be a valid handle".to_string(), 81 }), 82 ) 83 - .into_response()); 84 } 85 Err(_) => { 86 - return Err(( 87 StatusCode::BAD_REQUEST, 88 Json(ErrorResponse { 89 error: "InvalidRequest".to_string(), 90 message: "Error: handle must be a valid handle".to_string(), 91 }), 92 ) 93 - .into_response()); 94 } 95 }; 96 97 if validating { 98 - return Ok(StatusCode::NO_CONTENT.into_response()); 99 } 100 101 if queueing { ··· 112 } 113 } 114 115 - return Ok(StatusCode::NO_CONTENT.into_response()); 116 } 117 118 tracing::debug!(handle, "Resolving handle"); 119 120 - let if_none_match = headers.get(header::IF_NONE_MATCH); 121 122 - let (mut response, etag) = match handle_resolver.resolve(&handle).await { 123 - Ok((did, _timestamp)) => { 124 tracing::debug!(handle, did, "Found cached DID for handle"); 125 - 126 - // Calculate the weak etag for the successful response 127 let etag = calculate_etag(&did, app_context.etag_seed()); 128 - 129 - // Check if the client's etag matches our calculated one 130 - if if_none_match.is_some_and(|value| value == &etag) { 131 - (StatusCode::NOT_MODIFIED.into_response(), etag) 132 - } else { 133 - (Json(ResolveHandleResponse { did }).into_response(), etag) 134 } 135 } 136 Err(err) => { 137 tracing::debug!(error = ?err, handle, "Error resolving handle"); 138 - 139 - // Calculate the weak etag for the error response 140 - // Use a combination of error message and handle for consistent error etags 141 let error_content = format!("error:{}:{}", handle, err); 142 let etag = calculate_etag(&error_content, app_context.etag_seed()); 143 - 144 - if if_none_match.is_some_and(|value| value == &etag) { 145 - (StatusCode::NOT_MODIFIED.into_response(), etag) 146 - } else { 147 - ( 148 - ( 149 - StatusCode::BAD_REQUEST, 150 - Json(ErrorResponse { 151 - error: "InvalidRequest".to_string(), 152 - message: "Unable to resolve handle".to_string(), 153 - }), 154 - ) 155 - .into_response(), 156 - etag, 157 - ) 158 } 159 } 160 }; 161 162 - let headers = response.headers_mut(); 163 - 164 - // Add ETag header 165 - match HeaderValue::from_str(&etag) { 166 - Ok(etag_header_value) => { 167 - headers.insert(header::ETAG, etag_header_value); 168 - } 169 - Err(err) => { 170 - tracing::error!(error = ?err, "unable to create etag response value"); 171 - } 172 - } 173 - 174 - // Add Cache-Control header if configured 175 - if let Some(cache_control) = app_context.cache_control_header() 176 - && let Ok(cache_control_value) = HeaderValue::from_str(cache_control) 177 - { 178 - headers.insert(header::CACHE_CONTROL, cache_control_value); 179 } 180 181 - // Add CORS and Allow headers 182 - headers.insert("Allow", HeaderValue::from_static("GET, HEAD, OPTIONS")); 183 - headers.insert( 184 - header::ACCESS_CONTROL_ALLOW_HEADERS, 185 - HeaderValue::from_static("*"), 186 - ); 187 - headers.insert( 188 - header::ACCESS_CONTROL_ALLOW_METHODS, 189 - HeaderValue::from_static("GET, HEAD, OPTIONS"), 190 - ); 191 - headers.insert( 192 - header::ACCESS_CONTROL_ALLOW_ORIGIN, 193 - HeaderValue::from_static("*"), 194 - ); 195 - headers.insert( 196 - header::ACCESS_CONTROL_EXPOSE_HEADERS, 197 - HeaderValue::from_static("*"), 198 - ); 199 - headers.insert( 200 - header::ACCESS_CONTROL_MAX_AGE, 201 - HeaderValue::from_static("86400"), 202 - ); 203 - headers.insert( 204 - "Access-Control-Request-Headers", 205 - HeaderValue::from_static("*"), 206 - ); 207 - headers.insert( 208 - "Access-Control-Request-Method", 209 - HeaderValue::from_static("GET"), 210 - ); 211 212 - Ok(response) 213 } 214 215 - pub(super) async fn handle_xrpc_resolve_handle_options() -> Response { 216 - let mut response = StatusCode::NO_CONTENT.into_response(); 217 - let headers = response.headers_mut(); 218 - 219 // Add CORS and Allow headers for OPTIONS request 220 headers.insert("Allow", HeaderValue::from_static("GET, HEAD, OPTIONS")); 221 headers.insert( ··· 246 "Access-Control-Request-Method", 247 HeaderValue::from_static("GET"), 248 ); 249 - 250 - response 251 }
··· 1 + use chrono::{DateTime, Utc}; 2 use std::sync::Arc; 3 + use std::time::{SystemTime, UNIX_EPOCH}; 4 5 use crate::{ 6 handle_resolver::HandleResolver, ··· 36 message: String, 37 } 38 39 + /// Represents the result of a handle resolution 40 + enum ResolutionResult { 41 + Success { 42 + did: String, 43 + timestamp: u64, 44 + etag: String, 45 + }, 46 + Error { 47 + error: String, 48 + message: String, 49 + timestamp: u64, 50 + etag: String, 51 + }, 52 + } 53 + 54 + struct ResolutionResultView { 55 + result: ResolutionResult, 56 + cache_control: Option<String>, 57 + if_none_match: Option<HeaderValue>, 58 + if_modified_since: Option<HeaderValue>, 59 + } 60 + 61 + impl IntoResponse for ResolutionResultView { 62 + fn into_response(self) -> Response { 63 + let (last_modified, etag) = match &self.result { 64 + ResolutionResult::Success { 65 + timestamp, etag, .. 66 + } => (*timestamp, etag), 67 + ResolutionResult::Error { 68 + timestamp, etag, .. 69 + } => (*timestamp, etag), 70 + }; 71 + 72 + let mut headers = HeaderMap::new(); 73 + 74 + // WARNING: this swallows errors 75 + if let Ok(etag_value) = HeaderValue::from_str(etag) { 76 + headers.insert(header::ETAG, etag_value); 77 + } 78 + 79 + // Add Last-Modified header 80 + let last_modified_date = format_http_date(last_modified); 81 + // WARNING: this swallows errors 82 + if let Ok(last_modified_value) = HeaderValue::from_str(&last_modified_date) { 83 + headers.insert(header::LAST_MODIFIED, last_modified_value); 84 + } 85 + 86 + // Add Cache-Control header if configured 87 + if let Some(cache_control) = &self.cache_control { 88 + // WARNING: this swallows errors 89 + if let Ok(cache_control_value) = HeaderValue::from_str(cache_control) { 90 + headers.insert(header::CACHE_CONTROL, cache_control_value); 91 + } 92 + } 93 + 94 + headers.insert("Allow", HeaderValue::from_static("GET, HEAD, OPTIONS")); 95 + headers.insert( 96 + header::ACCESS_CONTROL_ALLOW_HEADERS, 97 + HeaderValue::from_static("*"), 98 + ); 99 + headers.insert( 100 + header::ACCESS_CONTROL_ALLOW_METHODS, 101 + HeaderValue::from_static("GET, HEAD, OPTIONS"), 102 + ); 103 + headers.insert( 104 + header::ACCESS_CONTROL_ALLOW_ORIGIN, 105 + HeaderValue::from_static("*"), 106 + ); 107 + headers.insert( 108 + header::ACCESS_CONTROL_EXPOSE_HEADERS, 109 + HeaderValue::from_static("*"), 110 + ); 111 + headers.insert( 112 + header::ACCESS_CONTROL_MAX_AGE, 113 + HeaderValue::from_static("86400"), 114 + ); 115 + headers.insert( 116 + "Access-Control-Request-Headers", 117 + HeaderValue::from_static("*"), 118 + ); 119 + headers.insert( 120 + "Access-Control-Request-Method", 121 + HeaderValue::from_static("GET"), 122 + ); 123 + 124 + if let ResolutionResult::Success { .. } = self.result { 125 + let fresh = self 126 + .if_modified_since 127 + .and_then(|inner_header_value| match inner_header_value.to_str() { 128 + Ok(value) => Some(value.to_string()), 129 + Err(_) => None, 130 + }) 131 + .and_then(|inner_str_value| parse_http_date(&inner_str_value)) 132 + .is_some_and(|inner_if_modified_since| last_modified <= inner_if_modified_since); 133 + 134 + if fresh { 135 + return (StatusCode::NOT_MODIFIED, headers).into_response(); 136 + } 137 + } 138 + 139 + let fresh = self 140 + .if_none_match 141 + .is_some_and(|if_none_match_value| if_none_match_value == etag); 142 + if fresh { 143 + return (StatusCode::NOT_MODIFIED, headers).into_response(); 144 + } 145 + 146 + match &self.result { 147 + ResolutionResult::Success { did, .. } => ( 148 + StatusCode::OK, 149 + headers, 150 + Json(ResolveHandleResponse { did: did.clone() }), 151 + ) 152 + .into_response(), 153 + ResolutionResult::Error { error, message, .. } => ( 154 + StatusCode::BAD_REQUEST, 155 + headers, 156 + Json(ErrorResponse { 157 + error: error.clone(), 158 + message: message.clone(), 159 + }), 160 + ) 161 + .into_response(), 162 + } 163 + 164 + // (status_code, headers).into_response() 165 + } 166 + } 167 + 168 /// Calculate a weak ETag for the given content using MetroHash64 with a seed 169 fn calculate_etag(content: &str, seed: &str) -> String { 170 let mut hasher = MetroHash64::new(); ··· 174 format!("W/\"{:x}\"", hash) 175 } 176 177 + /// Format a UNIX timestamp as an HTTP date string (RFC 7231) 178 + fn format_http_date(timestamp: u64) -> String { 179 + let datetime = DateTime::from_timestamp(timestamp as i64, 0).unwrap_or_else(Utc::now); 180 + 181 + datetime.format("%a, %d %b %Y %H:%M:%S GMT").to_string() 182 + } 183 + 184 + /// Parse an HTTP date string (RFC 7231) into a UNIX timestamp 185 + fn parse_http_date(date_str: &str) -> Option<u64> { 186 + use chrono::{DateTime, Utc}; 187 + 188 + // Try parsing with the standard HTTP date format 189 + DateTime::parse_from_rfc2822(date_str) 190 + .ok() 191 + .map(|dt| dt.with_timezone(&Utc).timestamp() as u64) 192 + } 193 + 194 pub(super) async fn handle_xrpc_resolve_handle( 195 headers: HeaderMap, 196 Query(params): Query<ResolveHandleParams>, 197 State(app_context): State<AppContext>, 198 State(handle_resolver): State<Arc<dyn HandleResolver>>, 199 State(queue): State<Arc<dyn QueueAdapter<HandleResolutionWork>>>, 200 + ) -> impl IntoResponse { 201 let validating = params.validate.is_some(); 202 let queueing = params.queue.is_some(); 203 ··· 205 let handle = match params.handle { 206 Some(h) => h, 207 None => { 208 + return ( 209 StatusCode::BAD_REQUEST, 210 Json(ErrorResponse { 211 error: "InvalidRequest".to_string(), 212 message: "Error: Params must have the property \"handle\"".to_string(), 213 }), 214 ) 215 + .into_response(); 216 } 217 }; 218 ··· 221 Ok(InputType::Handle(value)) => value, 222 Ok(InputType::Plc(_)) | Ok(InputType::Web(_)) => { 223 // It's a DID, not a handle 224 + return ( 225 StatusCode::BAD_REQUEST, 226 Json(ErrorResponse { 227 error: "InvalidRequest".to_string(), 228 message: "Error: handle must be a valid handle".to_string(), 229 }), 230 ) 231 + .into_response(); 232 } 233 Err(_) => { 234 + return ( 235 StatusCode::BAD_REQUEST, 236 Json(ErrorResponse { 237 error: "InvalidRequest".to_string(), 238 message: "Error: handle must be a valid handle".to_string(), 239 }), 240 ) 241 + .into_response(); 242 } 243 }; 244 245 if validating { 246 + return StatusCode::NO_CONTENT.into_response(); 247 } 248 249 if queueing { ··· 260 } 261 } 262 263 + return StatusCode::NO_CONTENT.into_response(); 264 } 265 266 tracing::debug!(handle, "Resolving handle"); 267 268 + // Get conditional request headers 269 + let if_none_match = headers.get(header::IF_NONE_MATCH).cloned(); 270 + let if_modified_since = headers.get(header::IF_MODIFIED_SINCE).cloned(); 271 272 + // Perform the resolution and build the response 273 + let result = match handle_resolver.resolve(&handle).await { 274 + Ok((did, timestamp)) => { 275 tracing::debug!(handle, did, "Found cached DID for handle"); 276 let etag = calculate_etag(&did, app_context.etag_seed()); 277 + ResolutionResult::Success { 278 + did, 279 + timestamp, 280 + etag, 281 } 282 } 283 Err(err) => { 284 tracing::debug!(error = ?err, handle, "Error resolving handle"); 285 let error_content = format!("error:{}:{}", handle, err); 286 let etag = calculate_etag(&error_content, app_context.etag_seed()); 287 + let timestamp = SystemTime::now() 288 + .duration_since(UNIX_EPOCH) 289 + .unwrap_or_default() 290 + .as_secs(); 291 + ResolutionResult::Error { 292 + error: "InvalidRequest".to_string(), 293 + message: "Unable to resolve handle".to_string(), 294 + timestamp, 295 + etag, 296 } 297 } 298 }; 299 300 + ResolutionResultView { 301 + result, 302 + cache_control: app_context.cache_control_header().map(|s| s.to_string()), 303 + if_none_match, 304 + if_modified_since, 305 } 306 + .into_response() 307 308 + // Build the response using the builder 309 + // let response = HandleResponseBuilder::new( 310 + // result, 311 + // app_context.cache_control_header().map(|s| s.to_string()), 312 + // if_none_match, 313 + // if_modified_since, 314 + // ) 315 + // .build(); 316 317 + // Ok(response) 318 } 319 320 + pub(super) async fn handle_xrpc_resolve_handle_options() -> impl IntoResponse { 321 + let mut headers = HeaderMap::new(); 322 + 323 // Add CORS and Allow headers for OPTIONS request 324 headers.insert("Allow", HeaderValue::from_static("GET, HEAD, OPTIONS")); 325 headers.insert( ··· 350 "Access-Control-Request-Method", 351 HeaderValue::from_static("GET"), 352 ); 353 + 354 + (StatusCode::NO_CONTENT, headers) 355 }