You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
6.5 KiB
Rust

6 years ago
//! A crate to help you fetch and serve WebFinger resources.
5 years ago
//!
6 years ago
//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.
use reqwest::{header::ACCEPT, Client};
use serde::{Serialize, Deserialize};
6 years ago
6 years ago
#[cfg(test)]
mod tests;
6 years ago
/// WebFinger result that may serialized or deserialized to JSON
6 years ago
#[derive(Debug, Serialize, Deserialize, PartialEq)]
6 years ago
pub struct Webfinger {
6 years ago
/// The subject of this WebFinger result.
5 years ago
///
6 years ago
/// It is an `acct:` URI
pub subject: String,
/// A list of aliases for this WebFinger result.
#[serde(default)]
6 years ago
pub aliases: Vec<String>,
/// Links to places where you may find more information about this resource.
5 years ago
pub links: Vec<Link>,
6 years ago
}
6 years ago
/// Structure to represent a WebFinger link
6 years ago
#[derive(Debug, Serialize, Deserialize, PartialEq)]
6 years ago
pub struct Link {
6 years ago
/// 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>,
6 years ago
/// The mime-type of this link.
5 years ago
///
6 years ago
/// If you fetch this URL, you may want to use this value for the Accept header of your HTTP
/// request.
5 years ago
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
6 years ago
}
6 years ago
/// An error that occured while fetching a WebFinger resource.
6 years ago
#[derive(Debug, PartialEq)]
6 years ago
pub enum WebfingerError {
6 years ago
/// The error came from the HTTP client.
6 years ago
HttpError,
6 years ago
/// The requested resource couldn't be parsed, and thus couldn't be fetched
6 years ago
ParseError,
6 years ago
/// The received JSON couldn't be parsed into a valid [`Webfinger`] struct.
5 years ago
JsonError,
6 years ago
}
/// 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.clone(),
}
}
}
/// 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
///
5 years ago
pub fn url_for(
prefix: Prefix,
acct: impl Into<String>,
with_https: bool,
) -> Result<String, WebfingerError> {
6 years ago
let acct = acct.into();
5 years ago
let scheme = if with_https { "https" } else { "http" };
let prefix: String = prefix.into();
6 years ago
acct.split("@")
6 years ago
.nth(1)
6 years ago
.ok_or(WebfingerError::ParseError)
5 years ago
.map(|instance| {
format!(
"{}://{}/.well-known/webfinger?resource={}:{}",
scheme, instance, prefix, acct
)
})
6 years ago
}
/// Fetches a WebFinger resource, identified by the `acct` parameter, a Webfinger URI.
pub async fn resolve_with_prefix(
5 years ago
prefix: Prefix,
acct: impl Into<String>,
with_https: bool,
) -> Result<Webfinger, WebfingerError> {
let url = url_for(prefix, acct, with_https)?;
6 years ago
Client::new()
.get(&url[..])
.header(ACCEPT, "application/jrd+json, application/json")
6 years ago
.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)?;
5 years ago
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
5 years ago
} else {
// fallback to acct:
resolve_with_prefix(Prefix::Acct, first, with_https).await
}
}
6 years ago
}
6 years ago
/// An error that occured while handling an incoming WebFinger request.
6 years ago
#[derive(Debug, PartialEq)]
6 years ago
pub enum ResolverError {
6 years ago
/// The requested resource was not correctly formatted
6 years ago
InvalidResource,
6 years ago
/// The website of the resource is not the current one.
WrongDomain,
6 years ago
/// The requested resource was not found.
5 years ago
NotFound,
6 years ago
}
6 years ago
/// A trait to easily generate a WebFinger endpoint for any resource repository.
5 years ago
///
6 years ago
/// 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.
6 years ago
pub trait Resolver<R> {
6 years ago
/// Returns the domain name of the current instance.
fn instance_domain<'a>(&self) -> &'a str;
6 years ago
/// Tries to find a resource, `acct`, in the repository `resource_repo`.
5 years ago
///
6 years ago
/// `acct` is not a complete `acct:` URI, it only contains the identifier of the requested resource
/// (e.g. `test` for `acct:test@example.org`)
5 years ago
///
6 years ago
/// If the resource couldn't be found, you may probably want to return a [`ResolverError::NotFound`].
fn find(&self, prefix: Prefix, acct: String, resource_repo: R) -> Result<Webfinger, ResolverError>;
6 years ago
6 years ago
/// Returns a WebFinger result for a requested resource.
fn endpoint(&self, resource: impl Into<String>, resource_repo: R) -> Result<Webfinger, ResolverError> {
6 years ago
let resource = resource.into();
let mut parsed_query = resource.splitn(2, ':');
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
let mut parsed_res = res.splitn(2, '@');
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
if domain == self.instance_domain() {
self.find(res_prefix, user.to_string(), resource_repo)
} else {
Err(ResolverError::WrongDomain)
}
6 years ago
}
}