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}