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
//! A crate to help you fetch and serve WebFinger resources.
//! 
//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.

extern crate reqwest;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use reqwest::{header::ACCEPT, Client};

#[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.
    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
    pub href: Option<String>,

    /// The Link may also contain an URL template, instead of an actual URL
    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")]
    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
}

/// Computes the URL to fetch for an `acct:` URI.
/// 
/// # Example
/// 
/// ```rust
/// use webfinger::url_for_acct;
/// 
/// assert_eq!(url_for_acct("test@example.org", true), Ok(String::from("https://example.org/.well-known/webfinger?resource=acct:test@example.org")));
/// ```
pub fn url_for_acct<T: Into<String>>(acct: T, with_https: bool) -> Result<String, WebfingerError> {
    let acct = acct.into();
    let scheme = if with_https {
        "https"
    } else {
        "http"
    };

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

/// Fetches a WebFinger resource, identified by the `acct` parameter, an `acct:` URI.
pub fn resolve<T: Into<String>>(acct: T, with_https: bool) -> Result<Webfinger, WebfingerError> {
    let url = url_for_acct(acct, with_https)?;
    Client::new()
        .get(&url[..])
        .header(ACCEPT, "application/jrd+json")
        .send()
        .map_err(|_| WebfingerError::HttpError)
        .and_then(|mut r| r.text().map_err(|_| WebfingerError::HttpError))
        .and_then(|res| serde_json::from_str(&res[..]).map_err(|_| WebfingerError::JsonError))
}

/// 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.
    WrongInstance,

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

/// A trait to easily generate a WebFinger endpoint for any resource repository.
/// 
/// The `R` type is your resource repository (a database for instance) that will be passed to the
/// [`find`](Resolver::find) and [`endpoint`](Resolver::endpoint) functions.
pub trait Resolver<R> {
    /// Returns the domain name of the current instance.
    fn instance_domain<'a>() -> &'a str;

    /// Tries to find a resource, `acct`, in the repository `resource_repo`.
    /// 
    /// `acct` is not a complete `acct:` URI, it only contains the identifier of the requested resource
    /// (e.g. `test` for `acct:test@example.org`)
    /// 
    /// If the resource couldn't be found, you may probably want to return a [`ResolverError::NotFound`].
    fn find(acct: String, resource_repo: R) -> Result<Webfinger, ResolverError>;

    /// Returns a WebFinger result for a requested resource.
    fn endpoint<T: Into<String>>(resource: T, resource_repo: R) -> Result<Webfinger, ResolverError> {
        let resource = resource.into();
        let mut parsed_query = resource.splitn(2, ":");
        parsed_query.next()
            .ok_or(ResolverError::InvalidResource)
            .and_then(|res_type| {
                if res_type == "acct" {
                    parsed_query.next().ok_or(ResolverError::InvalidResource)
                } else {
                    Err(ResolverError::InvalidResource)
                }
            })
            .and_then(|res| {
                let mut parsed_res = res.split("@");
                parsed_res.next()
                    .ok_or(ResolverError::InvalidResource)
                    .and_then(|user| {
                        parsed_res.next()
                            .ok_or(ResolverError::InvalidResource)
                            .and_then(|res_domain| {
                                if res_domain == Self::instance_domain() {
                                    Self::find(user.to_string(), resource_repo)
                                } else {
                                    Err(ResolverError::WrongInstance)
                                }
                            })
                    })
            })
    }
}