actix_webfinger/lib.rs
1//! # Actix Webfinger
2//! A library to aid in resolving and providing webfinger objects with the Actix Web web framework.
3//!
4//! The main functionality this crate provides is through the `Webfinger::fetch` method for Actix
5//! Web-based clients, and the `Resolver` trait for Actix Web-based servers.
6//!
7//! ### Usage
8//! First, add Actix Webfinger as a dependency
9//!
10//! - [Read the documentation on docs.rs](https://docs.rs/actix-webfinger)
11//! - [Find the crate on crates.io](https://crates.io/crates/actix-webfinger)
12//! - [Hit me up on Mastodon](https://asonix.dog/@asonix)
13//!
14//! ```toml
15//! [dependencies]
16//! actix-rt = "2.6.0"
17//! actix-web = "4.0.0"
18//! actix-webfinger = "0.4.0"
19//! ```
20//!
21//! Then use it in your application
22//!
23//! #### Client Example
24//! ```rust,ignore
25//! use actix_webfinger::Webfinger;
26//! use awc::Client;
27//! use std::error::Error;
28//!
29//! #[actix_rt::main]
30//! async fn main() -> Result<(), Box<dyn Error>> {
31//! let client = Client::default();
32//! let wf = Webfinger::fetch(
33//! &client,
34//! Some("acct:"),
35//! "asonix@localhost:8000",
36//! "localhost:8000",
37//! false,
38//! )
39//! .await?;
40//!
41//! println!("asonix's webfinger:\n{:#?}", wf);
42//! Ok(())
43//! }
44//! ```
45//!
46//! #### Server Example
47//! ```rust,ignore
48//! use actix_web::{middleware::Logger, web::Data, App, HttpServer};
49//! use actix_webfinger::{Resolver, Webfinger};
50//! use std::{error::Error, future::Future, pin::Pin};
51//!
52//! #[derive(Clone, Debug)]
53//! pub struct MyState {
54//! domain: String,
55//! }
56//!
57//! pub struct MyResolver;
58//!
59//! type LocalBoxFuture<'a, Output> = Pin<Box<dyn Future<Output = Output> + 'a>>;
60//!
61//! impl Resolver for MyResolver {
62//! type State = Data<MyState>;
63//! type Error = actix_web::error::JsonPayloadError;
64//!
65//! fn find(
66//! scheme: Option<&str>,
67//! account: &str,
68//! domain: &str,
69//! state: Data<MyState>,
70//! ) -> LocalBoxFuture<'static, Result<Option<Webfinger>, Self::Error>> {
71//! let w = if scheme == Some("acct:") && domain == state.domain {
72//! Some(Webfinger::new(&format!("{}@{}", account, domain)))
73//! } else {
74//! None
75//! };
76//!
77//! Box::pin(async move { Ok(w) })
78//! }
79//! }
80//!
81//! #[actix_rt::main]
82//! async fn main() -> Result<(), Box<dyn Error>> {
83//! std::env::set_var("RUST_LOG", "info");
84//! pretty_env_logger::init();
85//! HttpServer::new(|| {
86//! App::new()
87//! .app_data(Data::new(MyState {
88//! domain: "localhost:8000".to_owned(),
89//! }))
90//! .wrap(Logger::default())
91//! .service(actix_webfinger::resource::<MyResolver>())
92//! })
93//! .bind("127.0.0.1:8000")?
94//! .run()
95//! .await?;
96//!
97//! Ok(())
98//! }
99//! ```
100//!
101//! ### Contributing
102//! Feel free to open issues for anything you find an issue with. Please note that any contributed code will be licensed under the GPLv3.
103//!
104//! ### License
105//!
106//! Copyright © 2020 Riley Trautman
107//!
108//! Actix Webfinger is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
109//!
110//! Actix Webfinger is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. This file is part of Tokio ZMQ.
111//!
112//! You should have received a copy of the GNU General Public License along with Actix Webfinger. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/).
113use actix_web::{
114 error::ResponseError,
115 guard::{Guard, GuardContext},
116 http::Method,
117 web::{get, Query},
118 FromRequest, HttpResponse, Resource,
119};
120#[cfg(feature = "client")]
121use awc::Client;
122use serde_derive::{Deserialize, Serialize};
123use std::{future::Future, pin::Pin};
124
125/// A predicate for Actix Web route filters
126///
127/// This predicate matches GET requests with valid Accept headers. A valid Accept header is any
128/// Accept headers that matches a superset of `application/jrd+json`.
129///
130/// Valid Accept Headers
131/// - `application/jrd+json'
132/// - `application/json`
133/// - `application/*`
134/// - `*/*`
135///
136/// ```rust,ignore
137/// use actix_web::App;
138/// use actix_webfinger::WebfingerGuard;
139///
140/// let app = App::new()
141/// .resource("/.well-known/webfinger", |r| {
142/// r.route()
143/// .filter(WebfingerGuard)
144/// .with(your_route_handler)
145/// })
146/// .finish();
147/// ```
148pub struct WebfingerGuard;
149
150impl Guard for WebfingerGuard {
151 fn check(&self, ctx: &GuardContext<'_>) -> bool {
152 let valid_accept = if let Some(val) = ctx.head().headers().get("Accept") {
153 if let Ok(s) = val.to_str() {
154 s.split(',').any(|v| {
155 let v = if let Some(index) = v.find(';') {
156 v.split_at(index).0
157 } else {
158 v
159 };
160 let trimmed = v.trim();
161
162 // The following accept mimes are valid
163 trimmed == "application/jrd+json"
164 || trimmed == "application/json"
165 || trimmed == "application/*"
166 || trimmed == "*/*"
167 })
168 } else {
169 // unparsable accept headers are not valid
170 false
171 }
172 } else {
173 // no accept header is valid i guess
174 true
175 };
176
177 valid_accept && ctx.head().method == Method::GET
178 }
179}
180
181/// A simple way to mount the webfinger service to your Actix Web application
182///
183/// ```rust,ignore
184/// use actix_web::HttpServer;
185///
186/// HttpServer::new(|| {
187/// App::new()
188/// .service(actix_webfinger::resource::<MyResolver>())
189/// })
190/// .bind("127.0.0.1:8000")?
191/// .start();
192/// ```
193pub fn resource<R>() -> Resource
194where
195 R: Resolver + 'static,
196{
197 actix_web::web::resource("/.well-known/webfinger")
198 .guard(WebfingerGuard)
199 .route(get().to(endpoint::<R>))
200}
201
202/// A simple way to mount the webfinger service inside the /.well-known scope
203///
204/// ```rust,ignore
205/// use actix_web::{App, HttpServer, web::scope};
206/// use actix_webfinger::resource;
207///
208/// HttpServer::new(|| {
209/// App::new()
210/// .data(())
211/// .service(
212/// scope("/")
213/// .service(resource::<MyResolver>())
214/// )
215/// })
216/// .bind("127.0.0.1:8000")
217/// .start();
218/// ```
219pub fn scoped<R>() -> Resource
220where
221 R: Resolver + 'static,
222{
223 actix_web::web::resource("/webfinger")
224 .guard(WebfingerGuard)
225 .route(get().to(endpoint::<R>))
226}
227
228/// The error created if the webfinger resource query is malformed
229///
230/// Resource queries should have a valid `username@instance` format.
231///
232/// The following resource formats will not produce errors
233/// - `acct:asonix@asonix.dog`
234/// - `acct:@asonix@asonix.dog`
235/// - `asonix@asonix.dog`
236/// - `@asonix@asonix.dog`
237///
238/// The following resource formats will produce errors
239/// - `@asonix`
240/// - `asonix`
241///
242/// This error type captures the invalid string for inspection
243#[derive(Clone, Debug, thiserror::Error)]
244#[error("Resource {0} is invalid")]
245pub struct InvalidResource(String);
246
247/// A type representing a valid resource query
248///
249/// Resource queries should have a valid `username@instance` format.
250///
251/// The following resource formats will not produce errors
252/// - `acct:asonix@asonix.dog`
253/// - `acct:@asonix@asonix.dog`
254/// - `asonix@asonix.dog`
255/// - `@asonix@asonix.dog`
256///
257/// The following resource formats will produce errors
258/// - `@asonix`
259/// - `asonix`
260///
261/// This type implements `FromStr` and `serde::de::Deserialize` so it can be used to enforce valid
262/// formatting before the request reaches the route handler.
263#[derive(Clone, Debug)]
264pub struct WebfingerResource {
265 pub scheme: Option<String>,
266 pub account: String,
267 pub domain: String,
268}
269
270impl std::str::FromStr for WebfingerResource {
271 type Err = InvalidResource;
272
273 fn from_str(s: &str) -> Result<Self, Self::Err> {
274 let (scheme, trimmed) = s
275 .find(':')
276 .map(|index| {
277 let (scheme, trimmed) = s.split_at(index);
278 (
279 Some(scheme.to_owned() + ":"),
280 trimmed.trim_start_matches(':'),
281 )
282 })
283 .unwrap_or((None, s));
284
285 let trimmed = trimmed.trim_start_matches('@');
286
287 if let Some(index) = trimmed.find('@') {
288 let (account, domain) = trimmed.split_at(index);
289
290 Ok(WebfingerResource {
291 scheme,
292 account: account.to_owned(),
293 domain: domain.trim_start_matches('@').to_owned(),
294 })
295 } else {
296 Err(InvalidResource(s.to_owned()))
297 }
298 }
299}
300
301impl<'de> serde::de::Deserialize<'de> for WebfingerResource {
302 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
303 where
304 D: serde::de::Deserializer<'de>,
305 {
306 let s = String::deserialize(deserializer)?;
307 s.parse::<WebfingerResource>()
308 .map_err(serde::de::Error::custom)
309 }
310}
311
312/// A wrapper type for a Webfinger Resource
313///
314/// This type is used to deserialize from queries like the following:
315/// - `resource=acct:asonix@asonix.dog`
316///
317/// This can be used in Actix Web with the following code:
318///
319/// ```rust,ignore
320/// use actix_web::Query;
321/// use actix_webfinger::{WebfingerQuery, WebfingerResource};
322///
323/// fn my_route(query: Query<WebfingerQuery>) -> String {
324/// let WebfingerResource {
325/// account,
326/// domain,
327/// } = query.into_inner().resource;
328///
329/// // do things
330/// String::from("got resource")
331/// }
332/// ```
333#[derive(Clone, Debug, Deserialize)]
334pub struct WebfingerQuery {
335 resource: WebfingerResource,
336}
337
338/// A trait to ease the implementation of Webfinger Resolvers
339///
340/// ```rust,ignore
341/// use actix_webfinger::{Resolver, Webfinger};
342/// use std::{future::Future, pin::Pin};
343///
344/// struct MyResolver;
345///
346/// impl Resolver for MyResolver {
347/// type State = ();
348/// type Error = CustomError;
349///
350/// fn find(
351/// account: &str,
352/// domain: &str,
353/// _state: Self::State,
354/// ) -> Pin<Box<dyn Future<Output = Result<Option<Webfinger>, Self::Error>>>> {
355/// let webfinger = Webfinger::new(&format!("{}@{}", account, domain));
356///
357/// // do something
358///
359/// Box::pin(async move { Ok(Some(webfinger)) })
360/// }
361/// }
362///
363/// #[actix_rt::main]
364/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
365/// HttpServer::new(|| {
366/// App::new()
367/// .data(())
368/// .service(resource::<MyResolver>())
369/// })
370/// .bind("127.0.0.1:8000")?
371/// .run()
372/// .await?;
373///
374/// Ok(())
375/// }
376/// ```
377pub trait Resolver {
378 type State: FromRequest + 'static;
379 type Error: ResponseError + 'static;
380
381 fn find(
382 scheme: Option<&str>,
383 account: &str,
384 domain: &str,
385 state: Self::State,
386 ) -> Pin<Box<dyn Future<Output = WebfingerResult<Self::Error>>>>;
387}
388type WebfingerResult<E> = Result<Option<Webfinger>, E>;
389
390pub fn endpoint<R>(
391 (query, state): (Query<WebfingerQuery>, R::State),
392) -> Pin<Box<dyn Future<Output = Result<HttpResponse, R::Error>>>>
393where
394 R: Resolver,
395{
396 let WebfingerResource {
397 scheme,
398 account,
399 domain,
400 } = query.into_inner().resource;
401
402 Box::pin(async move {
403 match R::find(scheme.as_deref(), &account, &domain, state).await? {
404 Some(w) => Ok(w.respond()),
405 None => Ok(HttpResponse::NotFound().finish()),
406 }
407 })
408}
409
410/// The webfinger Link type
411///
412/// All links have a `rel` the determines what the link is describing, most have a `type` that adds
413/// some more context, and a `href` to point to, or contain the data.
414///
415/// In some cases, the Link can have a `rel` and a `template` and nothing else.
416///
417/// This type can be serialized and deserialized
418#[derive(Clone, Debug, Deserialize, Serialize)]
419pub struct Link {
420 pub rel: String,
421 #[serde(skip_serializing_if = "Option::is_none")]
422 pub href: Option<String>,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub template: Option<String>,
425
426 /// renamed to `type` via serde
427 #[serde(rename = "type")]
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub kind: Option<String>,
430}
431
432/// The webfinger type
433///
434/// Webfinger data has three parts
435/// - aliases
436/// - links
437/// - subject
438///
439/// `subject` defines the name or primary identifier of the object being looked up.
440/// `aliases` are alternate names for the object.
441/// `links` are references to more information about the subject, and can be in a variety of
442/// formats. For example, some links will reference activitypub person objects, and others will
443/// contain public key data.
444///
445/// This type can be serialized and deserialized
446#[derive(Clone, Debug, Deserialize, Serialize)]
447pub struct Webfinger {
448 pub aliases: Vec<String>,
449 pub links: Vec<Link>,
450 pub subject: String,
451}
452
453impl Webfinger {
454 /// Create a new Webfinger with the given subject
455 pub fn new(subject: &str) -> Self {
456 Webfinger {
457 aliases: Vec::new(),
458 links: Vec::new(),
459 subject: subject.to_owned(),
460 }
461 }
462
463 /// Add multiple aliases to the Webfinger
464 pub fn add_aliases(&mut self, aliases: &[String]) -> &mut Self {
465 self.aliases.extend_from_slice(aliases);
466 self
467 }
468
469 /// Add a single alias to the Webfinger
470 pub fn add_alias(&mut self, alias: &str) -> &mut Self {
471 self.aliases.push(alias.to_owned());
472 self
473 }
474
475 /// Get the aliases for this Webfinger
476 pub fn aliases(&self) -> &[String] {
477 &self.aliases
478 }
479
480 /// Add multiple Links to this Webfinger
481 pub fn add_links(&mut self, links: &[Link]) -> &mut Self {
482 self.links.extend_from_slice(links);
483 self
484 }
485
486 /// Add single Link to this Webfinger
487 pub fn add_link(&mut self, link: Link) -> &mut Self {
488 self.links.push(link);
489 self
490 }
491
492 /// Get the Links from this Webfinger
493 pub fn links(&self) -> &[Link] {
494 &self.links
495 }
496
497 /// Add an ActivityPub link to this Webfinger
498 ///
499 /// Since ActivityPub extends JsonLD, this also adds an ActivityStreams JsonLD link
500 pub fn add_activitypub(&mut self, href: &str) -> &mut Self {
501 self.links.push(Link {
502 rel: "self".to_owned(),
503 kind: Some("application/activity+json".to_owned()),
504 href: Some(href.to_owned()),
505 template: None,
506 });
507 self.add_json_ld(href, "https://www.w3.org/ns/activitystreams")
508 }
509
510 /// Add a JsonLD Link to this Webfinger
511 pub fn add_json_ld(&mut self, href: &str, profile: &str) -> &mut Self {
512 self.links.push(Link {
513 rel: "self".to_owned(),
514 kind: Some(format!("application/ld+json; profile=\"{}\"", profile)),
515 href: Some(href.to_owned()),
516 template: None,
517 });
518 self
519 }
520
521 /// Get an ActivityPub link from this Webfinger
522 pub fn activitypub(&self) -> Option<&Link> {
523 self.links.iter().find(|l| {
524 l.rel == "self"
525 && l.kind
526 .as_ref()
527 .map(|k| k == "application/activity+json")
528 .unwrap_or(false)
529 })
530 }
531
532 /// Get a JsonLD link from this Webfinger
533 pub fn json_ld(&self) -> Option<&Link> {
534 self.links.iter().find(|l| {
535 l.rel == "self"
536 && l.kind
537 .as_ref()
538 .map(|k| k.starts_with("application/ld+json"))
539 .unwrap_or(false)
540 })
541 }
542
543 /// Add a profile link to this Webfinger
544 pub fn add_profile(&mut self, href: &str) -> &mut Self {
545 self.links.push(Link {
546 rel: "http://webfinger.net/rel/profile-page".to_owned(),
547 href: Some(href.to_owned()),
548 kind: Some("text/html".to_owned()),
549 template: None,
550 });
551 self
552 }
553
554 /// Get a profile link from this Webfinger
555 pub fn profile(&self) -> Option<&Link> {
556 self.links
557 .iter()
558 .find(|l| l.rel == "http://webfinger.net/rel/profile-page")
559 }
560
561 /// Add an atom link to this Webfinger
562 pub fn add_atom(&mut self, href: &str) -> &mut Self {
563 self.links.push(Link {
564 rel: "http://schemas.google.com/g/2010#updates-from".to_owned(),
565 href: Some(href.to_owned()),
566 kind: Some("application/atom+xml".to_owned()),
567 template: None,
568 });
569 self
570 }
571
572 /// Get an atom link from this Webfinger
573 pub fn atom(&self) -> Option<&Link> {
574 self.links
575 .iter()
576 .find(|l| l.rel == "http://schemas.google.com/g/2010#updates-from")
577 }
578
579 /// Set a salmon link from this Webfinger
580 pub fn add_salmon(&mut self, href: &str) -> &mut Self {
581 self.links.push(Link {
582 rel: "salmon".to_owned(),
583 href: Some(href.to_owned()),
584 kind: None,
585 template: None,
586 });
587 self
588 }
589
590 /// Get a salmon link from this Webfinger
591 pub fn salmon(&self) -> Option<&Link> {
592 self.links.iter().find(|l| l.rel == "salmon")
593 }
594
595 /// Set a magic public key link for this Webfinger
596 pub fn add_magic_public_key(&mut self, magic_public_key: &str) -> &mut Self {
597 self.links.push(Link {
598 rel: "magic-public-key".to_owned(),
599 href: Some(format!(
600 "data:application/magic-public-key,{}",
601 magic_public_key
602 )),
603 kind: None,
604 template: None,
605 });
606 self
607 }
608
609 /// Get a magic public key link from this Webfinger
610 pub fn magic_public_key(&self) -> Option<&Link> {
611 self.links.iter().find(|l| l.rel == "magic-public-key")
612 }
613
614 /// Set an ostatus link for this Webfinger
615 pub fn add_ostatus(&mut self, template: &str) -> &mut Self {
616 self.links.push(Link {
617 rel: "http://ostatus.org/schema/1.0/subscribe".to_owned(),
618 href: None,
619 kind: None,
620 template: Some(template.to_owned()),
621 });
622 self
623 }
624
625 /// Get an ostatus link from this Webfinger
626 pub fn ostatus(&self) -> Option<&Link> {
627 self.links
628 .iter()
629 .find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe")
630 }
631
632 /// Turn this Webfinger into an actix web HttpResponse
633 pub fn respond(self) -> HttpResponse {
634 HttpResponse::Ok()
635 .content_type("application/jrd+json")
636 .json(&self)
637 }
638
639 #[cfg(feature = "client")]
640 /// Fetch a webfinger with subject `user` from a given `domain`
641 ///
642 /// This method takes a `Client` so derivative works can provide their own configured clients
643 /// rather this library generating it's own http clients.
644 pub async fn fetch(
645 client: &Client,
646 scheme: Option<&str>,
647 user: &str,
648 domain: &str,
649 https: bool,
650 ) -> Result<Self, FetchError> {
651 let url = format!(
652 "{}://{}/.well-known/webfinger?resource={}{}",
653 if https { "https" } else { "http" },
654 domain,
655 scheme.unwrap_or("acct:"),
656 user
657 );
658
659 let mut res = client
660 .get(url)
661 .append_header(("Accept", "application/jrd+json"))
662 .send()
663 .await
664 .map_err(|_| FetchError::Send)?;
665
666 res.json::<Webfinger>().await.map_err(|_| FetchError::Parse)
667 }
668}
669
670#[derive(Clone, Debug, thiserror::Error)]
671pub enum FetchError {
672 #[error("Failed to send request")]
673 Send,
674 #[error("Failed to parse response JSON")]
675 Parse,
676}
677
678#[cfg(test)]
679mod tests {
680 use crate::{Webfinger, WebfingerQuery};
681
682 const SIR_BOOPS: &str = r#"{"subject":"acct:Sir_Boops@sergal.org","aliases":["https://mastodon.sergal.org/@Sir_Boops","https://mastodon.sergal.org/users/Sir_Boops"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://mastodon.sergal.org/@Sir_Boops"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://mastodon.sergal.org/users/Sir_Boops.atom"},{"rel":"self","type":"application/activity+json","href":"https://mastodon.sergal.org/users/Sir_Boops"},{"rel":"salmon","href":"https://mastodon.sergal.org/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.vwDujxmxoYHs64MyVB3LG5ZyBxV3ufaMRBFu42bkcTpISq1WwZ-3Zb6CI8zOO-nM-Q2llrVRYjZa4ZFnOLvMTq_Kf-Zf5wy2aCRer88gX-MsJOAtItSi412y0a_rKOuFaDYLOLeTkRvmGLgZWbsrZJOp-YWb3zQ5qsIOInkc5BwI172tMsGeFtsnbNApPV4lrmtTGaJ8RiM8MR7XANBOfOHggSt1-eAIKGIsCmINEMzs1mG9D75xKtC_sM8GfbvBclQcBstGkHAEj1VHPW0ch6Bok5_QQppicyb8UA1PAA9bznSFtKlYE4xCH8rlCDSDTBRtdnBWHKcj619Ujz4Qaw==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://mastodon.sergal.org/authorize_interaction?uri={uri}"}]}"#;
683
684 const QUERIES: [&str; 4] = [
685 r#"{"resource":"acct:asonix@asonix.dog"}"#,
686 r#"{"resource":"asonix@asonix.dog"}"#,
687 r#"{"resource":"acct:@asonix@asonix.dog"}"#,
688 r#"{"resource":"@asonix@asonix.dog"}"#,
689 ];
690
691 #[test]
692 fn can_deserialize_sir_boops() {
693 let webfinger: Result<Webfinger, _> = serde_json::from_str(SIR_BOOPS);
694
695 assert!(webfinger.is_ok());
696
697 let webfinger = webfinger.unwrap();
698
699 assert!(webfinger.salmon().is_some());
700 assert!(webfinger.ostatus().is_some());
701 assert!(webfinger.activitypub().is_some());
702 assert!(webfinger.atom().is_some());
703 assert!(webfinger.magic_public_key().is_some());
704 assert!(webfinger.profile().is_some());
705 }
706
707 #[test]
708 fn can_deserialize_queries() {
709 for resource in &QUERIES {
710 let res: Result<WebfingerQuery, _> = serde_json::from_str(resource);
711
712 assert!(res.is_ok());
713
714 let query = res.unwrap();
715
716 assert_eq!(query.resource.account, "asonix");
717 assert_eq!(query.resource.domain, "asonix.dog");
718 }
719 }
720}