Skip to main content

webfinger_rs/
lib.rs

1//! `webfinger-rs` is a transport-agnostic [WebFinger] implementation for Rust, centered on the
2//! request and response types defined by [RFC 7033] with first-party integrations for [Reqwest],
3//! [Axum], and [Actix Web].
4//!
5//! WebFinger is used to discover information about people or other entities on the internet using
6//! URI-based identifiers such as `acct:carol@example.com`. In practice, it is commonly used for
7//! [OpenID Connect Discovery], account discovery in federated systems like [Mastodon] and
8//! [ActivityPub], and for publishing identity-related metadata from your own site or service.
9//!
10//! The crate keeps request parsing, JRD response construction, and framework adapters in one place
11//! so clients, servers, and tests use the same WebFinger types.
12//!
13//! # Why use `webfinger-rs`?
14//!
15//! - Reusable request and response types shaped around RFC 7033.
16//! - Optional Reqwest client execution via [`WebFingerRequest::execute_reqwest`].
17//! - Optional Axum and Actix Web extractor/responder integrations.
18//! - A permissive dual license (`MIT OR Apache-2.0`) that fits typical library and application
19//!   usage.
20//!
21//! [RFC 7033]: https://www.rfc-editor.org/rfc/rfc7033.html
22//! [WebFinger]: https://en.wikipedia.org/wiki/WebFinger
23//! [Reqwest]: https://crates.io/crates/reqwest
24//! [Axum]: https://crates.io/crates/axum
25//! [Actix Web]: https://crates.io/crates/actix-web
26//! [OpenID Connect Discovery]: https://openid.net/specs/openid-connect-discovery-1_0.html
27//! [Mastodon]: https://docs.joinmastodon.org/spec/webfinger/
28//! [ActivityPub]: https://www.w3.org/TR/activitypub/
29//! [RFC 7033 section 4.1]: https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1
30//! [RFC 7033 section 4.4]: https://www.rfc-editor.org/rfc/rfc7033.html#section-4.4
31//! [RFC 7033 section 10.1]: https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1
32//!
33//! # Install
34//!
35//! Start with the core crate, then enable the integration feature you need:
36//!
37//! ```shell
38//! cargo add webfinger-rs
39//! cargo add webfinger-rs --features reqwest
40//! cargo add webfinger-rs --features axum
41//! cargo add webfinger-rs --features actix
42//! ```
43//!
44//! The related CLI tool, [`webfinger-cli`], is useful for trying servers by hand:
45//!
46//! ```shell
47//! cargo install webfinger-cli
48//! webfinger acct:carol@example.com --rel http://webfinger.net/rel/avatar
49//! ```
50//!
51//! [`webfinger-cli`]: https://crates.io/crates/webfinger-cli
52//!
53//! # Feature matrix
54//!
55//! | Feature | What it enables |
56//! | --- | --- |
57//! | none | Core request/response types, builders, and URL conversion |
58//! | `reqwest` | Client execution helpers and Reqwest request/response conversions |
59//! | `axum` | [`WebFingerRequest`] extraction and [`WebFingerResponse`] responses in Axum via [`crate::axum`] |
60//! | `actix` | [`WebFingerRequest`] extraction and [`WebFingerResponse`] responses in Actix Web via [`crate::actix`] |
61//!
62//! # Primary types
63//!
64//! - [`WebFingerRequest`] models the WebFinger query target, host, and optional relation filters.
65//!   Build one directly for client requests, or extract one from an Axum or Actix handler.
66//! - [`WebFingerResponse`] models the JSON Resource Descriptor returned by a WebFinger endpoint.
67//!   Return one from server handlers or parse one from a Reqwest response.
68//! - [`Link`] and [`Rel`] model JRD link objects and relation filters so servers can apply the
69//!   same relation-filtering rules that clients request.
70//! - [`Resource`] and [`JrdUri`] validate URI-valued protocol fields before they enter requests or
71//!   JRD responses.
72//!
73//! # Protocol overview
74//!
75//! A WebFinger query is an HTTPS `GET` against the well-known endpoint
76//! [`WELL_KNOWN_PATH`] with a required `resource` parameter and, optionally, one or more `rel`
77//! parameters. The `resource` parameter is the query target URI; builders and server extractors
78//! reject relative references such as `carol`, `/relative`, `../x`, and empty values.
79//!
80//! A request built by this crate today for `acct:carol@example.com` filtered to the profile-page
81//! relation looks like this:
82//!
83//! ```text
84//! GET https://example.com/.well-known/webfinger?resource=acct%3Acarol%40example.com&rel=http%3A%2F%2Fwebfinger.net%2Frel%2Fprofile-page
85//! ```
86//!
87//! See: [RFC 7033 section 4.1] for the query-construction rules and percent-encoding details.
88//!
89//! Server integrations leave routing and TLS at the framework boundary, then use WebFinger
90//! extractors for protocol parsing:
91//!
92//! - mount the handler as `GET` at [`WELL_KNOWN_PATH`] so the router rejects other paths and
93//!   methods;
94//! - configure TLS and forwarded-proto handling at the server or reverse-proxy boundary; and
95//! - let the [`crate::axum`] or [`crate::actix`] extractor validate the request host, query
96//!   parameters, percent encoding, and `resource` URI.
97//!
98//! A successful JRD response might look like this:
99//!
100//! ```json
101//! {
102//!   "subject": "acct:carol@example.com",
103//!   "links": [
104//!     {
105//!       "rel": "http://webfinger.net/rel/profile-page",
106//!       "href": "https://example.com/users/carol"
107//!     }
108//!   ]
109//! }
110//! ```
111//!
112//! See: [RFC 7033 section 4.4] for the JRD structure.
113//!
114//! # Client quickstart
115//!
116//! Enable the `reqwest` feature to execute WebFinger requests directly from the request type.
117//! The current API expects an explicit host, which should normally match the resource host when the
118//! resource URI has one.
119//!
120//! ```rust,no_run
121//! # #[cfg(feature = "reqwest")] {
122//! use webfinger_rs::WebFingerRequest;
123//!
124//! const PROFILE_PAGE_REL: &str = "http://webfinger.net/rel/profile-page";
125//! const AVATAR_REL: &str = "http://webfinger.net/rel/avatar";
126//!
127//! async fn example() -> Result<(), Box<dyn std::error::Error>> {
128//!     let request = WebFingerRequest::builder("acct:carol@example.com")?
129//!         .host("example.com")
130//!         .rel(PROFILE_PAGE_REL)
131//!         .rel(AVATAR_REL)
132//!         .build();
133//!
134//!     let response = request.execute_reqwest().await?;
135//!     println!("Subject: {}", response.subject);
136//!     for rel in [PROFILE_PAGE_REL, AVATAR_REL] {
137//!         if let Some(href) = response
138//!             .links
139//!             .iter()
140//!             .find(|link| link.rel.as_ref() == rel)
141//!             .and_then(|link| link.href.as_ref().map(|href| href.as_ref()))
142//!         {
143//!             println!("{rel}: {href}");
144//!         }
145//!     }
146//!     println!("{response}");
147//!     Ok(())
148//! }
149//! # }
150//! ```
151//!
152//! # Axum quickstart
153//!
154//! Enable the `axum` feature to extract [`WebFingerRequest`] from the incoming request and return
155//! [`WebFingerResponse`] directly from your handler. Mount the handler at [`WELL_KNOWN_PATH`].
156//! See also [`crate::axum`] and the [Axum example].
157//!
158//! ```rust
159//! # #[cfg(feature = "axum")]
160//! # fn app() -> axum::Router {
161//! use axum::{http::StatusCode, routing::get, Router};
162//! use webfinger_rs::{Link, Rel, WELL_KNOWN_PATH, WebFingerRequest, WebFingerResponse};
163//!
164//! const SUBJECT: &str = "acct:carol@example.com";
165//! const PROFILE_PAGE_REL: &str = "http://webfinger.net/rel/profile-page";
166//! const AVATAR_REL: &str = "http://webfinger.net/rel/avatar";
167//! const PROFILE_URL: &str = "https://example.com/users/carol";
168//! const AVATAR_URL: &str = "https://example.com/media/carol.png";
169//! const ROLE_PROPERTY: &str = "https://example.com/ns/account-role";
170//!
171//! async fn webfinger(request: WebFingerRequest) -> axum::response::Result<WebFingerResponse> {
172//!     let subject = request.resource.to_string();
173//!     if subject != SUBJECT {
174//!         return Err((StatusCode::NOT_FOUND, "not found").into());
175//!     }
176//!
177//!     let mut links = Vec::new();
178//!
179//!     let profile_rel = Rel::new(PROFILE_PAGE_REL);
180//!     if request.rels.is_empty() || request.rels.contains(&profile_rel) {
181//!         links.push(
182//!             Link::builder(profile_rel)
183//!                 .href(PROFILE_URL)
184//!                 .title("en", "Carol's profile")
185//!                 .build(),
186//!         );
187//!     }
188//!
189//!     let avatar_rel = Rel::new(AVATAR_REL);
190//!     if request.rels.is_empty() || request.rels.contains(&avatar_rel) {
191//!         links.push(
192//!             Link::builder(avatar_rel)
193//!                 .href(AVATAR_URL)
194//!                 .r#type("image/png")
195//!                 .build(),
196//!         );
197//!     }
198//!
199//!     let response = WebFingerResponse::builder(subject)
200//!         .alias(PROFILE_URL)
201//!         .property(ROLE_PROPERTY, "maintainer")
202//!         .links(links)
203//!         .build();
204//!     Ok(response)
205//! }
206//!
207//! Router::new().route(WELL_KNOWN_PATH, get(webfinger))
208//! # }
209//! ```
210//!
211//! [Axum example]:
212//!     https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/axum.rs
213//!
214//! # Actix quickstart
215//!
216//! Enable the `actix` feature to use the same request and response types in Actix Web handlers.
217//! As with the Axum integration, the route path should be [`WELL_KNOWN_PATH`]. See also
218//! [`crate::actix`] and the [Actix example].
219//!
220//! ```rust
221//! # #[cfg(feature = "actix")]
222//! # fn app() -> actix_web::App<
223//! #     impl actix_web::dev::ServiceFactory<
224//! #         actix_web::dev::ServiceRequest,
225//! #         Config = (),
226//! #         Response = actix_web::dev::ServiceResponse,
227//! #         Error = actix_web::Error,
228//! #         InitError = (),
229//! #     >,
230//! # > {
231//! use actix_web::{App, web};
232//! use webfinger_rs::{Link, Rel, WELL_KNOWN_PATH, WebFingerRequest, WebFingerResponse};
233//!
234//! const SUBJECT: &str = "acct:carol@example.com";
235//! const PROFILE_PAGE_REL: &str = "http://webfinger.net/rel/profile-page";
236//! const AVATAR_REL: &str = "http://webfinger.net/rel/avatar";
237//! const PROFILE_URL: &str = "https://example.com/users/carol";
238//! const AVATAR_URL: &str = "https://example.com/media/carol.png";
239//! const ROLE_PROPERTY: &str = "https://example.com/ns/account-role";
240//!
241//! async fn webfinger(request: WebFingerRequest) -> actix_web::Result<WebFingerResponse> {
242//!     let subject = request.resource.to_string();
243//!     if subject != SUBJECT {
244//!         return Err(actix_web::error::ErrorNotFound("not found"));
245//!     }
246//!
247//!     let mut links = Vec::new();
248//!
249//!     let profile_rel = Rel::new(PROFILE_PAGE_REL);
250//!     if request.rels.is_empty() || request.rels.contains(&profile_rel) {
251//!         links.push(
252//!             Link::builder(profile_rel)
253//!                 .href(PROFILE_URL)
254//!                 .title("en", "Carol's profile")
255//!                 .build(),
256//!         );
257//!     }
258//!
259//!     let avatar_rel = Rel::new(AVATAR_REL);
260//!     if request.rels.is_empty() || request.rels.contains(&avatar_rel) {
261//!         links.push(
262//!             Link::builder(avatar_rel)
263//!                 .href(AVATAR_URL)
264//!                 .r#type("image/png")
265//!                 .build(),
266//!         );
267//!     }
268//!
269//!     let response = WebFingerResponse::builder(subject)
270//!         .alias(PROFILE_URL)
271//!         .property(ROLE_PROPERTY, "maintainer")
272//!         .links(links)
273//!         .build();
274//!     Ok(response)
275//! }
276//!
277//! App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger))
278//! # }
279//! ```
280//!
281//! [Actix example]:
282//!     https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
283//!
284//! # Compatibility
285//!
286//! The current first-party integration targets are:
287//!
288//! - Reqwest `0.13`
289//! - Axum `0.8`
290//! - Actix Web `4`
291//!
292//! The crate is currently pre-`0.1`, so API and compatibility adjustments may still land in minor
293//! releases while the integration surface settles. These version notes describe the currently
294//! integrated crates, not a full protocol-compliance matrix.
295//!
296//! # Limitations
297//!
298//! - Client execution is currently implemented only for Reqwest.
299//! - Server integrations are currently implemented only for Axum and Actix Web.
300//! - The crate focuses on RFC 7033 request/response handling and framework integration, not a full
301//!   identity stack around WebFinger.
302//! - The crate docs aim to stay grounded in RFC 7033, but they document the current implementation
303//!   rather than exhaustively enumerating every compliance detail.
304//!
305//! See: [RFC 7033 section 10.1] for the well-known path registration.
306//!
307//! # Examples
308//!
309//! Runnable examples are available in the repository:
310//!
311//! - `cargo run -p webfinger-rs --example axum --features axum`
312//! - `cargo run -p webfinger-rs --example actix --features actix`
313//! - `cargo run -p webfinger-rs --example client --features reqwest`
314//!
315//! Run one server example first, then run the client example in another shell. The client example
316//! queries `https://localhost:3000`, accepts the self-signed certificate generated by either server
317//! example, and prints the profile-page and avatar links returned by the shared
318//! [`WebFingerResponse`] type.
319//!
320//! The server examples also work with the CLI. Query without `--rel` to get both links, or pass a
321//! relation filter to narrow the returned `links` array:
322//!
323//! ```shell
324//! webfinger acct:carol@localhost localhost:3000 --insecure
325//! webfinger acct:carol@localhost localhost:3000 --insecure --rel http://webfinger.net/rel/profile-page
326//! webfinger acct:carol@localhost localhost:3000 --insecure --rel http://webfinger.net/rel/avatar
327//! ```
328//!
329//! # License
330//!
331//! Copyright (c) Josh McKinney
332//!
333//! This project is licensed under either of:
334//!
335//! - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
336//!   <https://apache.org/licenses/LICENSE-2.0>)
337//! - MIT license ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/licenses/MIT>) at your
338//!   option
339#![deny(missing_docs)]
340#![cfg_attr(docsrs, feature(doc_cfg))]
341
342pub use crate::error::Error;
343pub use crate::types::{
344    JrdUri, Link, LinkBuilder, Rel, Request as WebFingerRequest, RequestBuilder, Resource,
345    ResourceError, Response as WebFingerResponse, ResponseBuilder, Title,
346};
347
348#[cfg(feature = "actix")]
349pub mod actix;
350#[cfg(feature = "axum")]
351pub mod axum;
352mod error;
353mod http;
354#[cfg(any(feature = "actix", feature = "axum", test))]
355mod query;
356#[cfg(feature = "reqwest")]
357mod reqwest;
358mod types;
359
360/// The well-known path for WebFinger requests (`/.well-known/webfinger`).
361///
362/// This is the path that should be used to query for WebFinger resources.
363///
364/// See [RFC 7033 Section 10.1](https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1) for more
365/// information.
366pub const WELL_KNOWN_PATH: &str = "/.well-known/webfinger";