Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive Clone on all enums and structs + loosen requirements for instance_domain #13

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
[package]
authors = ["Ana Gelez <[email protected]>"]
name = "webfinger"
version = "0.5.1"
version = "0.6.0"
description = "A crate to help you fetch and serve WebFinger resources"
repository = "https://github.com/Plume-org/webfinger"
readme = "README.md"
keywords = ["webfinger", "federation", "decentralization"]
categories = ["web-programming"]
license = "GPL-3.0"
edition = "2018"
edition = "2021"

[features]
default = []
default = ["fetch"]
async = ["async-trait"]
fetch = ["dep:reqwest"]

[dependencies]
reqwest = { version = "0.11", features = [ "json" ] }
serde = { version = "1.0", features = [ "derive" ] }
async-trait = {version = "0.1.56", optional = true}
reqwest = { version = "0.11.12", features = ["json"], optional = true }
serde = { version = "1.0.147", features = ["derive"] }
async-trait = { version = "0.1.58", optional = true }

[dev-dependencies]
serde_json = "1.0"
mockito = "0.23"
tokio = { version = "1.19.2", features = [ "full" ] }
serde_json = "1.0.87"
mockito = "0.31.0"
tokio = { version = "1.21.2", features = ["full"] }
7 changes: 6 additions & 1 deletion src/async_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use async_trait::async_trait;
pub trait AsyncResolver {
type Repo: Send;
/// Returns the domain name of the current instance.
async fn instance_domain<'a>(&self) -> &'a str;
async fn instance_domain(&self) -> &str;

/// Tries to find a resource, `acct`, in the repository `resource_repo`.
///
Expand All @@ -25,6 +25,11 @@ pub trait AsyncResolver {
) -> Result<Webfinger, ResolverError>;

/// Returns a WebFinger result for a requested resource.
///
/// # Arguments
///
/// * `resource` - The resource to resolve.
/// * `resource_repo` - The resource repository.
async fn endpoint<R: Into<String> + Send>(
&self,
resource: R,
Expand Down
71 changes: 71 additions & 0 deletions src/fetch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use reqwest::{header::ACCEPT, Client};

use crate::*;

/// Computes the URL to fetch for a given resource.
///
/// # Parameters
///
/// - `prefix`: the resource prefix
/// - `acct`: the identifier of the resource, for instance: `[email protected]`
/// - `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
}
}
99 changes: 21 additions & 78 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Use [`resolve`] to fetch remote resources, and [`Resolver`] to serve your own resources.

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

mod resolver;
Expand All @@ -13,11 +13,16 @@ mod async_resolver;
#[cfg(feature = "async")]
pub use crate::async_resolver::*;

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

#[cfg(test)]
mod tests;

/// WebFinger result that may serialized or deserialized to JSON
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Webfinger {
/// The subject of this WebFinger result.
///
Expand All @@ -33,7 +38,7 @@ pub struct Webfinger {
}

/// Structure to represent a WebFinger link
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
pub struct Link {
/// Tells what this link represents
pub rel: String,
Expand All @@ -55,7 +60,7 @@ pub struct Link {
}

/// An error that occured while fetching a WebFinger resource.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum WebfingerError {
/// The error came from the HTTP client.
HttpError,
Expand All @@ -68,7 +73,7 @@ pub enum WebfingerError {
}

/// A prefix for a resource, either `acct:`, `group:` or some custom type.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Prefix {
/// `acct:` resource
Acct,
Expand All @@ -88,86 +93,24 @@ impl From<&str> for Prefix {
}
}

impl Into<String> for Prefix {
fn into(self) -> String {
match self {
Prefix::Acct => "acct".into(),
Prefix::Group => "group".into(),
Prefix::Custom(x) => x,
}
impl From<Prefix> for String {
fn from(prefix: Prefix) -> Self {
Cow::<'static, str>::from(prefix).into()
}
}

/// Computes the URL to fetch for a given resource.
///
/// # Parameters
///
/// - `prefix`: the resource prefix
/// - `acct`: the identifier of the resource, for instance: `[email protected]`
/// - `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
impl From<Prefix> for Cow<'static, str> {
fn from(prefix: Prefix) -> Self {
match prefix {
Prefix::Acct => "acct".into(),
Prefix::Group => "group".into(),
Prefix::Custom(x) => x.into(),
}
}
}

/// An error that occured while handling an incoming WebFinger request.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum ResolverError {
/// The requested resource was not correctly formatted
InvalidResource,
Expand Down
29 changes: 24 additions & 5 deletions src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{Prefix, ResolverError, Webfinger};
/// [`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;
fn instance_domain(&self) -> &str;

/// Tries to find a resource, `acct`, in the repository `resource_repo`.
///
Expand All @@ -17,26 +17,45 @@ pub trait Resolver<R> {
fn find(
&self,
prefix: Prefix,
acct: String,
acct: &str,
rels: &[impl AsRef<str>],
resource_repo: R,
) -> Result<Webfinger, ResolverError>;

/// Returns a WebFinger result for a requested resource.
///
/// # Arguments
///
/// * `resource` - The resource to resolve.
/// * `rels` - The relations to resolve.
/// As described in the [RFC](https://www.rfc-editor.org/rfc/rfc7033#section-4.3),
/// there may be zero or more rel parameters, which can be used to restrict the
/// set of links returned to those that have the specified relation type.
/// * `resource_repo` - The resource repository.
fn endpoint(
&self,
resource: impl Into<String>,
resource: &str,
rels: &[impl AsRef<str>],
resource_repo: R,
) -> Result<Webfinger, ResolverError> {
let resource = resource.into();
// Path for https://example.org/.well-known/webfinger/resource=acct:[email protected]&rel=http://openid.net/specs/connect/1.0/issuer
// resource = acct:[email protected]
// rel = http://openid.net/specs/connect/1.0/issuer
let mut parsed_query = resource.splitn(2, ':');
// parsed_query = ["acct", "[email protected]"]
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
// res_prefix = Prefix::Acct
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
// res = "[email protected]"

let mut parsed_res = res.splitn(2, '@');
// parsed_res = ["carol", "example.com"]
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
// user = "carol"
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
// domain = "example.com"
if domain == self.instance_domain() {
self.find(res_prefix, user.to_string(), resource_repo)
self.find(res_prefix, user, rels, resource_repo)
} else {
Err(ResolverError::WrongDomain)
}
Expand Down
Loading