webfinger/
lib.rs

1//! A crate to help you fetch and serve WebFinger resources.
2//!
3//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.
4
5use reqwest::{header::ACCEPT, Client};
6use serde::{Deserialize, Serialize};
7
8mod resolver;
9pub use crate::resolver::*;
10
11#[cfg(feature = "async")]
12mod async_resolver;
13#[cfg(feature = "async")]
14pub use crate::async_resolver::*;
15
16#[cfg(test)]
17mod tests;
18
19/// WebFinger result that may serialized or deserialized to JSON
20#[derive(Debug, Serialize, Deserialize, PartialEq)]
21pub struct Webfinger {
22    /// The subject of this WebFinger result.
23    ///
24    /// It is an `acct:` URI
25    pub subject: String,
26
27    /// A list of aliases for this WebFinger result.
28    #[serde(default)]
29    pub aliases: Vec<String>,
30
31    /// Links to places where you may find more information about this resource.
32    pub links: Vec<Link>,
33}
34
35/// Structure to represent a WebFinger link
36#[derive(Debug, Serialize, Deserialize, PartialEq)]
37pub struct Link {
38    /// Tells what this link represents
39    pub rel: String,
40
41    /// The actual URL of the link
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub href: Option<String>,
44
45    /// The Link may also contain an URL template, instead of an actual URL
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub template: Option<String>,
48
49    /// The mime-type of this link.
50    ///
51    /// If you fetch this URL, you may want to use this value for the Accept header of your HTTP
52    /// request.
53    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
54    pub mime_type: Option<String>,
55}
56
57/// An error that occured while fetching a WebFinger resource.
58#[derive(Debug, PartialEq)]
59pub enum WebfingerError {
60    /// The error came from the HTTP client.
61    HttpError,
62
63    /// The requested resource couldn't be parsed, and thus couldn't be fetched
64    ParseError,
65
66    /// The received JSON couldn't be parsed into a valid [`Webfinger`] struct.
67    JsonError,
68}
69
70/// A prefix for a resource, either `acct:`, `group:` or some custom type.
71#[derive(Debug, PartialEq)]
72pub enum Prefix {
73    /// `acct:` resource
74    Acct,
75    /// `group:` resource
76    Group,
77    /// Another type of resource
78    Custom(String),
79}
80
81impl From<&str> for Prefix {
82    fn from(s: &str) -> Prefix {
83        match s.to_lowercase().as_ref() {
84            "acct" => Prefix::Acct,
85            "group" => Prefix::Group,
86            x => Prefix::Custom(x.into()),
87        }
88    }
89}
90
91impl Into<String> for Prefix {
92    fn into(self) -> String {
93        match self {
94            Prefix::Acct => "acct".into(),
95            Prefix::Group => "group".into(),
96            Prefix::Custom(x) => x,
97        }
98    }
99}
100
101/// Computes the URL to fetch for a given resource.
102///
103/// # Parameters
104///
105/// - `prefix`: the resource prefix
106/// - `acct`: the identifier of the resource, for instance: `someone@example.org`
107/// - `with_https`: indicates wether the URL should be on HTTPS or HTTP
108///
109pub fn url_for(
110    prefix: Prefix,
111    acct: impl Into<String>,
112    with_https: bool,
113) -> Result<String, WebfingerError> {
114    let acct = acct.into();
115    let scheme = if with_https { "https" } else { "http" };
116
117    let prefix: String = prefix.into();
118    acct.split('@')
119        .nth(1)
120        .ok_or(WebfingerError::ParseError)
121        .map(|instance| {
122            format!(
123                "{}://{}/.well-known/webfinger?resource={}:{}",
124                scheme, instance, prefix, acct
125            )
126        })
127}
128
129/// Fetches a WebFinger resource, identified by the `acct` parameter, a Webfinger URI.
130pub async fn resolve_with_prefix(
131    prefix: Prefix,
132    acct: impl Into<String>,
133    with_https: bool,
134) -> Result<Webfinger, WebfingerError> {
135    let url = url_for(prefix, acct, with_https)?;
136    Client::new()
137        .get(&url[..])
138        .header(ACCEPT, "application/jrd+json, application/json")
139        .send()
140        .await
141        .map_err(|_| WebfingerError::HttpError)?
142        .json()
143        .await
144        .map_err(|_| WebfingerError::JsonError)
145}
146
147/// Fetches a Webfinger resource.
148///
149/// If the resource doesn't have a prefix, `acct:` will be used.
150pub async fn resolve(
151    acct: impl Into<String>,
152    with_https: bool,
153) -> Result<Webfinger, WebfingerError> {
154    let acct = acct.into();
155    let mut parsed = acct.splitn(2, ':');
156    let first = parsed.next().ok_or(WebfingerError::ParseError)?;
157
158    if first.contains('@') {
159        // This : was a port number, not a prefix
160        resolve_with_prefix(Prefix::Acct, acct, with_https).await
161    } else if let Some(other) = parsed.next() {
162        resolve_with_prefix(Prefix::from(first), other, with_https).await
163    } else {
164        // fallback to acct:
165        resolve_with_prefix(Prefix::Acct, first, with_https).await
166    }
167}
168
169/// An error that occured while handling an incoming WebFinger request.
170#[derive(Debug, PartialEq)]
171pub enum ResolverError {
172    /// The requested resource was not correctly formatted
173    InvalidResource,
174
175    /// The website of the resource is not the current one.
176    WrongDomain,
177
178    /// The requested resource was not found.
179    NotFound,
180}