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";