diff --git a/Cargo.toml b/Cargo.toml index e58c889..1c85a0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,26 @@ [package] authors = ["Ana Gelez "] 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"] } diff --git a/src/async_resolver.rs b/src/async_resolver.rs index 8a032e1..3273dec 100644 --- a/src/async_resolver.rs +++ b/src/async_resolver.rs @@ -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`. /// @@ -25,6 +25,11 @@ pub trait AsyncResolver { ) -> Result; /// Returns a WebFinger result for a requested resource. + /// + /// # Arguments + /// + /// * `resource` - The resource to resolve. + /// * `resource_repo` - The resource repository. async fn endpoint + Send>( &self, resource: R, diff --git a/src/fetch.rs b/src/fetch.rs new file mode 100644 index 0000000..7cd08f1 --- /dev/null +++ b/src/fetch.rs @@ -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: `someone@example.org` +/// - `with_https`: indicates wether the URL should be on HTTPS or HTTP +/// +pub fn url_for( + prefix: Prefix, + acct: impl Into, + with_https: bool, +) -> Result { + 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, + with_https: bool, +) -> Result { + 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, + with_https: bool, +) -> Result { + 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 + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 98fe726..7279ea7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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. /// @@ -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, @@ -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, @@ -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, @@ -88,86 +93,24 @@ impl From<&str> for Prefix { } } -impl Into for Prefix { - fn into(self) -> String { - match self { - Prefix::Acct => "acct".into(), - Prefix::Group => "group".into(), - Prefix::Custom(x) => x, - } +impl From 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: `someone@example.org` -/// - `with_https`: indicates wether the URL should be on HTTPS or HTTP -/// -pub fn url_for( - prefix: Prefix, - acct: impl Into, - with_https: bool, -) -> Result { - 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, - with_https: bool, -) -> Result { - 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, - with_https: bool, -) -> Result { - 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 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, diff --git a/src/resolver.rs b/src/resolver.rs index 3076b4e..139760d 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -6,7 +6,7 @@ use crate::{Prefix, ResolverError, Webfinger}; /// [`find`](Resolver::find) and [`endpoint`](Resolver::endpoint) functions. pub trait Resolver { /// 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`. /// @@ -17,26 +17,45 @@ pub trait Resolver { fn find( &self, prefix: Prefix, - acct: String, + acct: &str, + rels: &[impl AsRef], resource_repo: R, ) -> Result; /// 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, + resource: &str, + rels: &[impl AsRef], resource_repo: R, ) -> Result { - let resource = resource.into(); + // Path for https://example.org/.well-known/webfinger/resource=acct:carol@example.com&rel=http://openid.net/specs/connect/1.0/issuer + // resource = acct:carol@example.com + // rel = http://openid.net/specs/connect/1.0/issuer let mut parsed_query = resource.splitn(2, ':'); + // parsed_query = ["acct", "carol@example.com"] 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 = "carol@example.com" 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) } diff --git a/src/tests.rs b/src/tests.rs index b11bda3..2ece56d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,7 +1,9 @@ use super::*; +#[cfg(feature = "fetch")] use tokio::runtime::Runtime; #[test] +#[cfg(feature = "fetch")] fn test_url_for() { assert_eq!( url_for(Prefix::Acct, "test@example.org", true), @@ -34,6 +36,7 @@ fn test_url_for() { } #[test] +#[cfg(feature = "fetch")] fn test_resolve() { let r = Runtime::new().unwrap(); let m = mockito::mock("GET", mockito::Matcher::Any) @@ -151,20 +154,21 @@ pub struct MyResolver; // Only one user, represented by a String impl Resolver<&'static str> for MyResolver { - fn instance_domain<'a>(&self) -> &'a str { + fn instance_domain(&self) -> &str { "instance.tld" } fn find( &self, prefix: Prefix, - acct: String, + acct: &str, + rels: &[impl AsRef], resource_repo: &'static str, ) -> Result { if acct == resource_repo && prefix == Prefix::Acct { Ok(Webfinger { - subject: acct.clone(), - aliases: vec![acct.clone()], + subject: acct.to_owned(), + aliases: vec![acct.to_owned()], links: vec![Link { rel: "http://webfinger.net/rel/profile-page".to_string(), mime_type: None, @@ -217,31 +221,33 @@ impl AsyncResolver for MyAsyncResolver { #[test] fn test_my_resolver() { let resolver = MyResolver; + let rels = vec!["http://webfinger.net/rel/profile-page"]; + assert!(resolver - .endpoint("acct:admin@instance.tld", "admin") + .endpoint("acct:admin@instance.tld", &Vec::::new(), "admin") .is_ok()); assert_eq!( - resolver.endpoint("acct:test@instance.tld", "admin"), + resolver.endpoint("acct:test@instance.tld", &rels, "admin"), Err(ResolverError::NotFound) ); assert_eq!( - resolver.endpoint("acct:admin@oops.ie", "admin"), + resolver.endpoint("acct:admin@oops.ie", &rels, "admin"), Err(ResolverError::WrongDomain) ); assert_eq!( - resolver.endpoint("admin@instance.tld", "admin"), + resolver.endpoint("admin@instance.tld", &rels, "admin"), Err(ResolverError::InvalidResource) ); assert_eq!( - resolver.endpoint("admin", "admin"), + resolver.endpoint("admin", &rels, "admin"), Err(ResolverError::InvalidResource) ); assert_eq!( - resolver.endpoint("acct:admin", "admin"), + resolver.endpoint("acct:admin", &rels, "admin"), Err(ResolverError::InvalidResource) ); assert_eq!( - resolver.endpoint("group:admin@instance.tld", "admin"), + resolver.endpoint("group:admin@instance.tld", &rels, "admin"), Err(ResolverError::NotFound) ); }