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