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
206 lines
6.5 KiB
Rust
//! 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::{Serialize, Deserialize};
|
|
|
|
#[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.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
|
|
///
|
|
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,
|
|
}
|
|
|
|
/// 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>(&self) -> &'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(&self, prefix: Prefix, acct: String, resource_repo: R) -> Result<Webfinger, ResolverError>;
|
|
|
|
/// Returns a WebFinger result for a requested resource.
|
|
fn endpoint(&self, resource: impl Into<String>, resource_repo: R) -> Result<Webfinger, ResolverError> {
|
|
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)
|
|
}
|
|
}
|
|
}
|