webfinger_rs/actix.rs
1//! Actix Web integration for WebFinger request extraction and JRD responses.
2//!
3//! Enable the `actix` feature to:
4//!
5//! - extract [`WebFingerRequest`] from handlers mounted for `GET` requests to
6//! [`crate::WELL_KNOWN_PATH`]; and
7//! - return [`WebFingerResponse`] directly from Actix handlers as `application/jrd+json` with the
8//! WebFinger CORS header.
9//!
10//! The extractor reads the standard WebFinger query shape from [RFC 7033 section 4.1]:
11//!
12//! - a required `resource` query parameter; and
13//! - zero or more repeated `rel` query parameters.
14//!
15//! The `resource` value must be an absolute URI such as `acct:carol@example.com` or
16//! `https://example.com/users/carol`; relative references are rejected as malformed requests.
17//!
18//! In practice, route handlers should usually be mounted like this:
19//!
20//! ```rust
21//! use actix_web::{get, App};
22//! use webfinger_rs::{WELL_KNOWN_PATH, WebFingerRequest, WebFingerResponse};
23//!
24//! #[get("/.well-known/webfinger")]
25//! async fn webfinger(_request: WebFingerRequest) -> WebFingerResponse {
26//! WebFingerResponse::new("acct:carol@example.com")
27//! }
28//!
29//! let app = App::new().service(webfinger);
30//! # let _ = app;
31//! # assert_eq!(WELL_KNOWN_PATH, "/.well-known/webfinger");
32//! ```
33//!
34//! The Actix router owns path and method matching. Mounting the handler with `web::get()` or
35//! `#[get]` at [`crate::WELL_KNOWN_PATH`] rejects other paths and non-`GET` methods before this
36//! extractor runs. The extractor itself validates the WebFinger request metadata available inside
37//! the handler: host, query parameters, percent encoding, and the `resource` URI.
38//!
39//! RFC 7033 requires HTTPS for WebFinger. Actix request metadata does not reliably identify the
40//! externally visible scheme when the application runs behind TLS termination or a reverse proxy, so
41//! this extractor does not enforce scheme. Configure TLS and forwarded-proto handling at your
42//! server or proxy boundary.
43//!
44//! If extraction fails, Actix returns `400 Bad Request` for missing or duplicated `resource`,
45//! missing host values, invalid percent encoding, relative resource references, or invalid resource
46//! URIs.
47//!
48//! See also [`WebFingerRequest`] for the extractor impl, [`WebFingerResponse`] for the responder
49//! impl, and the [Actix example] for a runnable server.
50//!
51//! [RFC 7033 section 4.1]: https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1
52//! [Actix example]:
53//! https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
54
55use std::future::{Ready, ready};
56
57use actix_web::dev::Payload;
58use actix_web::error::ErrorBadRequest;
59use actix_web::http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, HeaderValue};
60use actix_web::web::Json;
61use actix_web::{Error as ActixError, FromRequest, HttpRequest, HttpResponse, Responder};
62use tracing::trace;
63
64use crate::http::CORS_ALLOW_ORIGIN;
65use crate::query::{RequestParams, RequestParamsError};
66use crate::{Rel, WebFingerRequest, WebFingerResponse};
67
68const CORS_ALLOW_ORIGIN_HEADER: HeaderValue = HeaderValue::from_static(CORS_ALLOW_ORIGIN);
69const JRD_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/jrd+json");
70
71impl Responder for WebFingerResponse {
72 /// Converts a [`WebFingerResponse`] into an Actix response.
73 ///
74 /// This serializes the body as JSON and sets the `Content-Type` header to
75 /// `application/jrd+json`, which is the JRD media type used by WebFinger.
76 /// It also sets `Access-Control-Allow-Origin: *` as recommended by RFC 7033 section 5.
77 ///
78 /// Handlers can therefore return [`WebFingerResponse`] directly without manually wrapping it in
79 /// [`actix_web::web::Json`] or setting the response header themselves.
80 ///
81 /// See also the [`crate::actix`] module docs and the [Actix example].
82 ///
83 /// # Example
84 ///
85 /// ```rust
86 /// use actix_web::{get, App};
87 /// use webfinger_rs::{Link, Rel, WebFingerRequest, WebFingerResponse};
88 ///
89 /// #[get("/.well-known/webfinger")]
90 /// async fn webfinger(request: WebFingerRequest) -> actix_web::Result<WebFingerResponse> {
91 /// let subject = request.resource.to_string();
92 /// let rel = Rel::new("http://webfinger.net/rel/profile-page");
93 /// let response = if request.rels.is_empty() || request.rels.contains(&rel) {
94 /// let link = Link::builder(rel).href("https://example.com/users/carol");
95 /// WebFingerResponse::builder(subject).link(link).build()
96 /// } else {
97 /// WebFingerResponse::builder(subject).build()
98 /// };
99 /// Ok(response)
100 /// }
101 ///
102 /// let app = App::new().service(webfinger);
103 /// # let _ = app;
104 /// ```
105 ///
106 /// [Actix example]:
107 /// https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
108 type Body = <Json<WebFingerResponse> as Responder>::Body;
109
110 fn respond_to(self, request: &HttpRequest) -> HttpResponse<Self::Body> {
111 let mut response = Json(self).respond_to(request);
112 response
113 .headers_mut()
114 .insert(ACCESS_CONTROL_ALLOW_ORIGIN, CORS_ALLOW_ORIGIN_HEADER);
115 response
116 .headers_mut()
117 .insert(CONTENT_TYPE, JRD_CONTENT_TYPE);
118 response
119 }
120}
121
122impl FromRequest for WebFingerRequest {
123 /// Extracts a [`WebFingerRequest`] from an Actix request.
124 ///
125 /// The extractor reads:
126 ///
127 /// - the host from the request URI authority or the HTTP `Host` header;
128 /// - the decoded `resource` query parameter; and
129 /// - every repeated decoded `rel` query parameter.
130 ///
131 /// Query parsing percent-decodes parameters while preserving RFC 3986 query semantics.
132 ///
133 /// # Errors
134 ///
135 /// - If the request has zero or more than one `resource` query parameter, extraction returns a
136 /// bad request.
137 /// - If the request has no URI authority and no `Host` header, extraction returns
138 /// `ErrorBadRequest("missing host")`.
139 /// - If the query contains malformed percent encoding, extraction returns a bad request.
140 /// - If `resource` is present but cannot be parsed as a URI, extraction returns
141 /// `ErrorBadRequest("invalid resource: ...")`.
142 ///
143 /// See also the [`crate::actix`] module docs and the [Actix example].
144 ///
145 /// # Example
146 ///
147 /// ```rust
148 /// use actix_web::{get, App};
149 /// use webfinger_rs::{WebFingerRequest, WebFingerResponse};
150 ///
151 /// #[get("/.well-known/webfinger")]
152 /// async fn webfinger(request: WebFingerRequest) -> WebFingerResponse {
153 /// WebFingerResponse::new(request.resource.to_string())
154 /// }
155 ///
156 /// let app = App::new().service(webfinger);
157 /// # let _ = app;
158 /// ```
159 ///
160 /// [Actix example]:
161 /// https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
162 type Error = ActixError;
163
164 type Future = Ready<Result<Self, Self::Error>>;
165
166 fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
167 trace!(?req, "extracting WebFingerRequest from request");
168 ready(extract_request(req))
169 }
170}
171
172/// Extracts WebFinger request data from Actix request metadata.
173///
174/// WebFinger request extraction only needs URI, host, and query metadata from RFC 7033 sections 4.1
175/// and 4.2, so the fallible work can stay synchronous and the Actix [`FromRequest`] implementation
176/// can wrap the result in a ready future.
177fn extract_request(req: &HttpRequest) -> Result<WebFingerRequest, ActixError> {
178 let host = req
179 .uri()
180 .host()
181 .or_else(|| req.headers().get("host").and_then(|h| h.to_str().ok()))
182 .map(|h| h.to_string())
183 .ok_or(ErrorBadRequest("missing host"))?;
184 let query: RequestParams = req.query_string().parse()?;
185 let rels = query
186 .rel
187 .into_iter()
188 .map(Rel::try_new)
189 .collect::<Result<Vec<_>, _>>()
190 .map_err(ErrorBadRequest)?;
191 Ok(WebFingerRequest {
192 host,
193 resource: query.resource,
194 rels,
195 })
196}
197
198impl From<RequestParamsError> for ActixError {
199 fn from(error: RequestParamsError) -> Self {
200 ErrorBadRequest(error)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use actix_web::body::to_bytes;
207 use actix_web::http::StatusCode;
208 use actix_web::{App, HttpResponse, test, web};
209
210 use super::*;
211 use crate::WELL_KNOWN_PATH;
212
213 type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
214
215 /// Returns the extracted resource so tests can assert RFC 7033 query decoding behavior.
216 ///
217 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
218 async fn webfinger(request: WebFingerRequest) -> HttpResponse {
219 HttpResponse::Ok().body(request.resource.to_string())
220 }
221
222 /// Returns extracted relation filters so tests can assert RFC 7033 repeated `rel` handling.
223 ///
224 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.3>.
225 async fn webfinger_rels(request: WebFingerRequest) -> HttpResponse {
226 let rels = request
227 .rels
228 .iter()
229 .map(ToString::to_string)
230 .collect::<Vec<_>>();
231 HttpResponse::Ok().json(rels)
232 }
233
234 /// Returns a minimal JRD so tests can assert responder-owned WebFinger headers.
235 async fn webfinger_response() -> WebFingerResponse {
236 WebFingerResponse::new("acct:carol@example.com")
237 }
238
239 /// Includes the RFC 7033 CORS header on successful JRD responses.
240 ///
241 /// WebFinger resources must be queryable from browsers, and RFC 7033 section 5 recommends the
242 /// least restrictive `Access-Control-Allow-Origin` value for public WebFinger resources.
243 ///
244 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-5>.
245 #[actix_web::test]
246 async fn successful_response_sets_cors_header() -> Result {
247 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_response));
248 let app = test::init_service(app).await;
249 let request = test::TestRequest::get().uri(WELL_KNOWN_PATH).to_request();
250
251 let response = test::call_service(&app, request).await;
252
253 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
254 assert_eq!(
255 response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN),
256 Some(&CORS_ALLOW_ORIGIN_HEADER),
257 );
258 Ok(())
259 }
260
261 /// Returns WebFinger responses with the registered JRD media type.
262 ///
263 /// RFC 7033 section 4.2 defines `application/jrd+json` as the media type for JSON Resource
264 /// Descriptor responses.
265 ///
266 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
267 #[actix_web::test]
268 async fn webfinger_response_uses_jrd_content_type() -> Result {
269 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_response));
270 let app = test::init_service(app).await;
271 let request = test::TestRequest::get().uri(WELL_KNOWN_PATH).to_request();
272
273 let response = test::call_service(&app, request).await;
274
275 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
276 assert_eq!(
277 response.headers().get(CONTENT_TYPE),
278 Some(&JRD_CONTENT_TYPE),
279 );
280 Ok(())
281 }
282
283 /// Accepts a percent-encoded `acct:` resource without panicking.
284 ///
285 /// The resource query value is percent-encoded under RFC 7033 section 4.1, then parsed as a
286 /// URI query target under RFC 7033 section 4.2.
287 ///
288 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
289 /// <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
290 #[actix_web::test]
291 async fn valid_percent_encoded_resource() -> Result {
292 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
293 let app = test::init_service(app).await;
294 let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
295 let request = test::TestRequest::get()
296 .uri(&uri)
297 .insert_header(("host", "example.org"))
298 .to_request();
299
300 let response = test::call_service(&app, request).await;
301
302 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
303 let body = to_bytes(response.into_body()).await?;
304 assert_eq!(body.as_ref(), b"acct:bad@example.org");
305 Ok(())
306 }
307
308 /// Relies on Actix routing to reject non-WebFinger paths before extraction.
309 ///
310 /// RFC 7033 sections 4 and 10.1 define `/.well-known/webfinger` as the WebFinger resource.
311 /// Path matching stays in the router so applications get normal Actix `404 Not Found` behavior.
312 ///
313 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4> and
314 /// <https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1>.
315 #[actix_web::test]
316 async fn wrong_path_is_not_routed() -> Result {
317 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
318 let app = test::init_service(app).await;
319 let request = test::TestRequest::get()
320 .uri("/webfinger?resource=acct%3Abad%40example.org")
321 .insert_header(("host", "example.org"))
322 .to_request();
323
324 let response = test::call_service(&app, request).await;
325
326 assert_eq!(response.status(), StatusCode::NOT_FOUND, "{response:?}");
327 Ok(())
328 }
329
330 /// Relies on Actix routing to reject non-`GET` requests before extraction.
331 ///
332 /// RFC 7033 section 4.2 specifies a `GET` request. Method matching stays in the router so
333 /// applications get normal Actix routing behavior for other methods.
334 ///
335 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
336 #[actix_web::test]
337 async fn wrong_method_is_not_routed() -> Result {
338 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
339 let app = test::init_service(app).await;
340 let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
341 let request = test::TestRequest::post()
342 .uri(&uri)
343 .insert_header(("host", "example.org"))
344 .to_request();
345
346 let response = test::call_service(&app, request).await;
347
348 assert_eq!(response.status(), StatusCode::NOT_FOUND, "{response:?}");
349 Ok(())
350 }
351
352 /// Converts malformed resource values into Actix bad-request responses.
353 ///
354 /// RFC 7033 section 4.2 requires absent or malformed `resource` parameters to be treated as bad
355 /// requests instead of panicking inside extraction.
356 ///
357 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
358 #[actix_web::test]
359 async fn request_with_invalid_resource() -> Result {
360 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
361 let app = test::init_service(app).await;
362 let uri = format!("{WELL_KNOWN_PATH}?resource=http%3A%2F%2F%5B%3A%3A1");
363 let request = test::TestRequest::get()
364 .uri(&uri)
365 .insert_header(("host", "example.org"))
366 .to_request();
367
368 let response = test::call_service(&app, request).await;
369
370 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
371 let body = to_bytes(response.into_body()).await?;
372 assert_eq!(body.as_ref(), b"invalid resource: invalid authority");
373 Ok(())
374 }
375
376 /// Rejects relative resource references at the Actix extractor boundary.
377 ///
378 /// RFC 7033 identifies the WebFinger query target as a URI, not a relative reference. Actix
379 /// handlers should not receive ambiguous targets such as local paths or bare names.
380 ///
381 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
382 /// <https://www.rfc-editor.org/rfc/rfc3986.html#section-4.1>.
383 #[actix_web::test]
384 async fn relative_resource_is_bad_request() -> Result {
385 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
386 let app = test::init_service(app).await;
387 let uri = format!("{WELL_KNOWN_PATH}?resource=/relative");
388 let request = test::TestRequest::get()
389 .uri(&uri)
390 .insert_header(("host", "example.org"))
391 .to_request();
392
393 let response = test::call_service(&app, request).await;
394
395 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
396 let body = to_bytes(response.into_body()).await?;
397 assert_eq!(
398 body.as_ref(),
399 b"invalid resource: resource must be an absolute URI",
400 );
401 Ok(())
402 }
403
404 /// Preserves repeated `rel` parameters instead of collapsing them.
405 ///
406 /// WebFinger clients use repeated `rel` keys to request multiple relation filters. A generic
407 /// map-shaped query parser can easily keep only one value, which would make handlers see an
408 /// incomplete request.
409 ///
410 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
411 #[actix_web::test]
412 async fn valid_request_with_repeated_rel_params() -> Result {
413 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
414 let app = test::init_service(app).await;
415 let resource = "acct%3Acarol%40example.org";
416 let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=profile&rel=avatar");
417 let request = test::TestRequest::get()
418 .uri(&uri)
419 .insert_header(("host", "example.org"))
420 .to_request();
421
422 let response = test::call_service(&app, request).await;
423
424 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
425 let body = to_bytes(response.into_body()).await?;
426 assert_eq!(body.as_ref(), br#"["profile","avatar"]"#);
427 Ok(())
428 }
429
430 /// Exposes decoded relation URIs to Actix handlers.
431 ///
432 /// The shared parser owns the RFC 3986 percent-decoding rule; this adapter test proves Actix
433 /// handlers receive decoded `Rel` values rather than raw query text.
434 ///
435 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
436 /// <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
437 #[actix_web::test]
438 async fn rel_params_are_percent_decoded() -> Result {
439 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
440 let app = test::init_service(app).await;
441 let resource = "acct%3Acarol%40example.org";
442 let rel = "http%3A%2F%2Fwebfinger.example%2Frel%2Fprofile-page";
443 let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel={rel}");
444 let request = test::TestRequest::get()
445 .uri(&uri)
446 .insert_header(("host", "example.org"))
447 .to_request();
448
449 let response = test::call_service(&app, request).await;
450
451 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
452 let body = to_bytes(response.into_body()).await?;
453 assert_eq!(
454 body.as_ref(),
455 br#"["http://webfinger.example/rel/profile-page"]"#,
456 );
457 Ok(())
458 }
459
460 /// Rejects relation values that are neither one registered relation type nor one URI.
461 ///
462 /// RFC 7033 section 4.4.4.1 allows one relation type per `rel` member. Multiple relation
463 /// filters should be encoded as repeated `rel` parameters, not as whitespace-separated values.
464 #[actix_web::test]
465 async fn invalid_rel_is_bad_request() -> Result {
466 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
467 let app = test::init_service(app).await;
468 let resource = "acct%3Acarol%40example.org";
469 let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=author%20avatar");
470 let request = test::TestRequest::get()
471 .uri(&uri)
472 .insert_header(("host", "example.org"))
473 .to_request();
474
475 let response = test::call_service(&app, request).await;
476
477 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
478 let body = to_bytes(response.into_body()).await?;
479 assert_eq!(body.as_ref(), b"invalid relation type: author avatar");
480 Ok(())
481 }
482
483 /// Converts invalid UTF-8 after percent decoding into an Actix bad-request response.
484 ///
485 /// The shared parser owns the byte-level validation; this adapter test proves malformed
486 /// percent-encoded bytes do not reach an Actix handler as relation strings.
487 ///
488 /// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
489 #[actix_web::test]
490 async fn invalid_percent_encoded_rel_is_bad_request() -> Result {
491 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
492 let app = test::init_service(app).await;
493 let resource = "acct%3Acarol%40example.org";
494 let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=%FF");
495 let request = test::TestRequest::get()
496 .uri(&uri)
497 .insert_header(("host", "example.org"))
498 .to_request();
499
500 let response = test::call_service(&app, request).await;
501
502 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
503 let body = to_bytes(response.into_body()).await?;
504 assert_eq!(body.as_ref(), b"invalid percent-encoded query parameter");
505 Ok(())
506 }
507
508 /// Rejects malformed percent escape syntax instead of treating `%` literally.
509 ///
510 /// The shared query parser owns the RFC 3986 check; this Actix test proves that parser errors are
511 /// converted into `400 Bad Request` responses instead of escaping the extractor boundary.
512 ///
513 /// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
514 #[actix_web::test]
515 async fn malformed_percent_escape_is_bad_request() -> Result {
516 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
517 let app = test::init_service(app).await;
518 let resource = "acct%3Acarol%40example.org";
519 let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=%GG");
520 let request = test::TestRequest::get()
521 .uri(&uri)
522 .insert_header(("host", "example.org"))
523 .to_request();
524
525 let response = test::call_service(&app, request).await;
526
527 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
528 let body = to_bytes(response.into_body()).await?;
529 assert_eq!(body.as_ref(), b"invalid percent-encoded query parameter");
530 Ok(())
531 }
532
533 /// Accepts `resource` in any query parameter position through the Actix extractor.
534 ///
535 /// RFC 7033 section 4.1 does not make parameter order significant. This adapter test proves
536 /// Actix handlers still receive relation filters when `resource` appears after them.
537 ///
538 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
539 #[actix_web::test]
540 async fn resource_parameter_order_does_not_matter() -> Result {
541 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
542 let app = test::init_service(app).await;
543 let resource = "acct%3Acarol%40example.org";
544 let uri = format!("{WELL_KNOWN_PATH}?rel=profile&resource={resource}");
545 let request = test::TestRequest::get()
546 .uri(&uri)
547 .insert_header(("host", "example.org"))
548 .to_request();
549
550 let response = test::call_service(&app, request).await;
551
552 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
553 let body = to_bytes(response.into_body()).await?;
554 assert_eq!(body.as_ref(), br#"["profile"]"#);
555 Ok(())
556 }
557
558 /// Keeps encoded `=` and `&` inside handler-visible resource values.
559 ///
560 /// Resource URIs may contain query strings of their own. This adapter test proves Actix receives
561 /// the decoded target resource without splitting encoded inner delimiters into WebFinger
562 /// parameters.
563 ///
564 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
565 #[actix_web::test]
566 async fn encoded_delimiters_stay_inside_resource() -> Result {
567 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
568 let app = test::init_service(app).await;
569 let resource = "https%3A%2F%2Fexample.org%2Fprofile%3Fa%3D1%26b%3D2";
570 let uri = format!("{WELL_KNOWN_PATH}?resource={resource}");
571 let request = test::TestRequest::get()
572 .uri(&uri)
573 .insert_header(("host", "example.org"))
574 .to_request();
575
576 let response = test::call_service(&app, request).await;
577
578 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
579 let body = to_bytes(response.into_body()).await?;
580 assert_eq!(body.as_ref(), b"https://example.org/profile?a=1&b=2");
581 Ok(())
582 }
583
584 /// Preserves literal `+` in Actix handler-visible resources.
585 ///
586 /// Actix's normal query extractor is not used here because WebFinger follows RFC 3986 query
587 /// semantics, where `+` remains data instead of becoming a space.
588 ///
589 /// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4>.
590 #[actix_web::test]
591 async fn plus_is_not_decoded_as_space() -> Result {
592 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
593 let app = test::init_service(app).await;
594 let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Acarol+tag%40example.org");
595 let request = test::TestRequest::get()
596 .uri(&uri)
597 .insert_header(("host", "example.org"))
598 .to_request();
599
600 let response = test::call_service(&app, request).await;
601
602 assert_eq!(response.status(), StatusCode::OK, "{response:?}");
603 let body = to_bytes(response.into_body()).await?;
604 assert_eq!(body.as_ref(), b"acct:carol+tag@example.org");
605 Ok(())
606 }
607
608 /// Rejects duplicate `resource` parameters at the Actix extractor boundary.
609 ///
610 /// The parser owns the RFC 7033 section 4.2 rule that there is exactly one target. This adapter
611 /// test proves ambiguous requests become `400 Bad Request` responses rather than arbitrary
612 /// handler inputs.
613 ///
614 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
615 #[actix_web::test]
616 async fn request_with_multiple_resources() -> Result {
617 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
618 let app = test::init_service(app).await;
619 let carol = "acct%3Acarol%40example.org";
620 let alice = "acct%3Aalice%40example.org";
621 let uri = format!("{WELL_KNOWN_PATH}?resource={carol}&resource={alice}");
622 let request = test::TestRequest::get()
623 .uri(&uri)
624 .insert_header(("host", "example.org"))
625 .to_request();
626
627 let response = test::call_service(&app, request).await;
628
629 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
630 let body = to_bytes(response.into_body()).await?;
631 assert_eq!(body.as_ref(), b"multiple resource parameters");
632 Ok(())
633 }
634
635 /// Rejects requests that omit the required `resource` parameter.
636 ///
637 /// The shared query parser owns the exact RFC 7033 rule; this Actix test proves that missing
638 /// `resource` is exposed as an Actix bad-request response.
639 ///
640 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
641 #[actix_web::test]
642 async fn request_with_missing_resource() -> Result {
643 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
644 let app = test::init_service(app).await;
645 let request = test::TestRequest::get()
646 .uri(WELL_KNOWN_PATH)
647 .insert_header(("host", "example.org"))
648 .to_request();
649
650 let response = test::call_service(&app, request).await;
651
652 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
653 let body = to_bytes(response.into_body()).await?;
654 assert_eq!(body.as_ref(), b"missing resource parameter");
655 Ok(())
656 }
657
658 /// Rejects requests where neither the URI nor `Host` header provides an authority.
659 ///
660 /// The request host is significant to WebFinger query routing.
661 ///
662 /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4>.
663 #[actix_web::test]
664 async fn request_with_no_host() -> Result {
665 let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
666 let app = test::init_service(app).await;
667 let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
668 let request = test::TestRequest::get().uri(&uri).to_request();
669
670 let response = test::call_service(&app, request).await;
671
672 assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
673 let body = to_bytes(response.into_body()).await?;
674 assert_eq!(body.as_ref(), b"missing host");
675 Ok(())
676 }
677}