+3
Cargo.lock
+3
Cargo.lock
···
314
314
dependencies = [
315
315
"android-tzdata",
316
316
"iana-time-zone",
317
+
"js-sys",
317
318
"num-traits",
319
+
"wasm-bindgen",
318
320
"windows-link",
319
321
]
320
322
···
1902
1904
"atproto-identity",
1903
1905
"axum",
1904
1906
"bincode",
1907
+
"chrono",
1905
1908
"deadpool-redis",
1906
1909
"metrohash",
1907
1910
"reqwest",
+1
Cargo.toml
+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
+
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
+196
-92
src/http/handle_xrpc_resolve_handle.rs
···
1
+
use chrono::{DateTime, Utc};
1
2
use std::sync::Arc;
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
+
/// 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
+
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
+
/// 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
+
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
-
) -> Result<Response, Response> {
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
-
return Err((
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
-
.into_response());
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
-
return Err((
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
-
.into_response());
231
+
.into_response();
84
232
}
85
233
Err(_) => {
86
-
return Err((
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
-
.into_response());
241
+
.into_response();
94
242
}
95
243
};
96
244
97
245
if validating {
98
-
return Ok(StatusCode::NO_CONTENT.into_response());
246
+
return StatusCode::NO_CONTENT.into_response();
99
247
}
100
248
101
249
if queueing {
···
112
260
}
113
261
}
114
262
115
-
return Ok(StatusCode::NO_CONTENT.into_response());
263
+
return StatusCode::NO_CONTENT.into_response();
116
264
}
117
265
118
266
tracing::debug!(handle, "Resolving handle");
119
267
120
-
let if_none_match = headers.get(header::IF_NONE_MATCH);
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();
121
271
122
-
let (mut response, etag) = match handle_resolver.resolve(&handle).await {
123
-
Ok((did, _timestamp)) => {
272
+
// Perform the resolution and build the response
273
+
let result = match handle_resolver.resolve(&handle).await {
274
+
Ok((did, timestamp)) => {
124
275
tracing::debug!(handle, did, "Found cached DID for handle");
125
-
126
-
// Calculate the weak etag for the successful response
127
276
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)
277
+
ResolutionResult::Success {
278
+
did,
279
+
timestamp,
280
+
etag,
134
281
}
135
282
}
136
283
Err(err) => {
137
284
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
285
let error_content = format!("error:{}:{}", handle, err);
142
286
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
-
)
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,
158
296
}
159
297
}
160
298
};
161
299
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);
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,
179
305
}
306
+
.into_response()
180
307
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
-
);
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();
211
316
212
-
Ok(response)
317
+
// Ok(response)
213
318
}
214
319
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
-
320
+
pub(super) async fn handle_xrpc_resolve_handle_options() -> impl IntoResponse {
321
+
let mut headers = HeaderMap::new();
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
-
250
-
response
353
+
354
+
(StatusCode::NO_CONTENT, headers)
251
355
}