1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! A crate to help you fetch and serve WebFinger resources.
//!
//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.

use reqwest::{header::ACCEPT, Client};
use serde::{Deserialize, Serialize};

mod resolver;
pub use crate::resolver::*;

#[cfg(feature = "async")]
mod async_resolver;
#[cfg(feature = "async")]
pub use crate::async_resolver::*;

#[cfg(test)]
mod tests;

/// WebFinger result that may serialized or deserialized to JSON
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Webfinger {
    /// The subject of this WebFinger result.
    ///
    /// It is an `acct:` URI
    pub subject: String,

    /// A list of aliases for this WebFinger result.
    #[serde(default)]
    pub aliases: Vec<String>,

    /// Links to places where you may find more information about this resource.
    pub links: Vec<Link>,
}

/// Structure to represent a WebFinger link
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Link {
    /// Tells what this link represents
    pub rel: String,

    /// The actual URL of the link
    #[serde(skip_serializing_if = "Option::is_none")]
    pub href: Option<String>,

    /// The Link may also contain an URL template, instead of an actual URL
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template: Option<String>,

    /// The mime-type of this link.
    ///
    /// If you fetch this URL, you may want to use this value for the Accept header of your HTTP
    /// request.
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub mime_type: Option<String>,
}

/// An error that occured while fetching a WebFinger resource.
#[derive(Debug, PartialEq)]
pub enum WebfingerError {
    /// The error came from the HTTP client.
    HttpError,

    /// The requested resource couldn't be parsed, and thus couldn't be fetched
    ParseError,

    /// The received JSON couldn't be parsed into a valid [`Webfinger`] struct.
    JsonError,
}

/// A prefix for a resource, either `acct:`, `group:` or some custom type.
#[derive(Debug, PartialEq)]
pub enum Prefix {
    /// `acct:` resource
    Acct,
    /// `group:` resource
    Group,
    /// Another type of resource
    Custom(String),
}

impl From<&str> for Prefix {
    fn from(s: &str) -> Prefix {
        match s.to_lowercase().as_ref() {
            "acct" => Prefix::Acct,
            "group" => Prefix::Group,
            x => Prefix::Custom(x.into()),
        }
    }
}

impl Into<String> for Prefix {
    fn into(self) -> String {
        match self {
            Prefix::Acct => "acct".into(),
            Prefix::Group => "group".into(),
            Prefix::Custom(x) => x,
        }
    }
}

/// Computes the URL to fetch for a given resource.
///
/// # Parameters
///
/// - `prefix`: the resource prefix
/// - `acct`: the identifier of the resource, for instance: `someone@example.org`
/// - `with_https`: indicates wether the URL should be on HTTPS or HTTP
///
pub fn url_for(
    prefix: Prefix,
    acct: impl Into<String>,
    with_https: bool,
) -> Result<String, WebfingerError> {
    let acct = acct.into();
    let scheme = if with_https { "https" } else { "http" };

    let prefix: String = prefix.into();
    acct.split('@')
        .nth(1)
        .ok_or(WebfingerError::ParseError)
        .map(|instance| {
            format!(
                "{}://{}/.well-known/webfinger?resource={}:{}",
                scheme, instance, prefix, acct
            )
        })
}

/// Fetches a WebFinger resource, identified by the `acct` parameter, a Webfinger URI.
pub async fn resolve_with_prefix(
    prefix: Prefix,
    acct: impl Into<String>,
    with_https: bool,
) -> Result<Webfinger, WebfingerError> {
    let url = url_for(prefix, acct, with_https)?;
    Client::new()
        .get(&url[..])
        .header(ACCEPT, "application/jrd+json, application/json")
        .send()
        .await
        .map_err(|_| WebfingerError::HttpError)?
        .json()
        .await
        .map_err(|_| WebfingerError::JsonError)
}

/// Fetches a Webfinger resource.
///
/// If the resource doesn't have a prefix, `acct:` will be used.
pub async fn resolve(
    acct: impl Into<String>,
    with_https: bool,
) -> Result<Webfinger, WebfingerError> {
    let acct = acct.into();
    let mut parsed = acct.splitn(2, ':');
    let first = parsed.next().ok_or(WebfingerError::ParseError)?;

    if first.contains('@') {
        // This : was a port number, not a prefix
        resolve_with_prefix(Prefix::Acct, acct, with_https).await
    } else if let Some(other) = parsed.next() {
        resolve_with_prefix(Prefix::from(first), other, with_https).await
    } else {
        // fallback to acct:
        resolve_with_prefix(Prefix::Acct, first, with_https).await
    }
}

/// An error that occured while handling an incoming WebFinger request.
#[derive(Debug, PartialEq)]
pub enum ResolverError {
    /// The requested resource was not correctly formatted
    InvalidResource,

    /// The website of the resource is not the current one.
    WrongDomain,

    /// The requested resource was not found.
    NotFound,
}