Add support for group: and custom prefixes (fixes #3)
+ Rust 2018 + Test for resolve
This commit is contained in:
parent
1b72b441e3
commit
2bb0133d41
7 changed files with 152 additions and 59 deletions
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.travis.yml
Normal file → Executable file
0
.travis.yml
Normal file → Executable file
11
Cargo.toml
Normal file → Executable file
11
Cargo.toml
Normal file → Executable file
|
@ -1,15 +1,20 @@
|
|||
[package]
|
||||
authors = ["Baptiste Gelez <baptiste@gelez.xyz>"]
|
||||
authors = ["Ana Gelez <ana@gelez.xyz>"]
|
||||
name = "webfinger"
|
||||
version = "0.3.1"
|
||||
description = "A crate to help you fetch and serve WebFinger resources"
|
||||
repository = "https://github.com/BaptisteGelez/webfinger"
|
||||
repository = "https://github.com/Plume-org/webfinger"
|
||||
readme = "README.md"
|
||||
keywords = ["webfinger"]
|
||||
keywords = ["webfinger", "federation", "decentralization"]
|
||||
categories = ["web-programming"]
|
||||
license = "GPL-3.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
reqwest = "0.9"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
mockito = "0.16"
|
0
LICENSE
Normal file → Executable file
0
LICENSE
Normal file → Executable file
0
README.md
Normal file → Executable file
0
README.md
Normal file → Executable file
132
src/lib.rs
Normal file → Executable file
132
src/lib.rs
Normal file → Executable file
|
@ -2,11 +2,7 @@
|
|||
//!
|
||||
//! 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;
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
use reqwest::{header::ACCEPT, Client};
|
||||
|
||||
|
@ -61,16 +57,47 @@ pub enum WebfingerError {
|
|||
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> {
|
||||
|
||||
/// 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"
|
||||
|
@ -78,22 +105,41 @@ pub fn url_for_acct<T: Into<String>>(acct: T, with_https: bool) -> Result<String
|
|||
"http"
|
||||
};
|
||||
|
||||
let prefix: String = prefix.into();
|
||||
acct.split("@")
|
||||
.nth(1)
|
||||
.ok_or(WebfingerError::ParseError)
|
||||
.map(|instance| format!("{}://{}/.well-known/webfinger?resource=acct:{}", scheme, instance, acct))
|
||||
.map(|instance| format!("{}://{}/.well-known/webfinger?resource={}:{}", scheme, instance, prefix, 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)?;
|
||||
/// Fetches a WebFinger resource, identified by the `acct` parameter, a Webfinger URI.
|
||||
pub 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")
|
||||
.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))
|
||||
.and_then(|mut r| r.json().map_err(|_| WebfingerError::JsonError))
|
||||
}
|
||||
|
||||
/// Fetches a Webfinger resource.
|
||||
///
|
||||
/// If the resource doesn't have a prefix, `acct:` will be used.
|
||||
pub 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)
|
||||
} else {
|
||||
if let Some(other) = parsed.next() {
|
||||
resolve_with_prefix(Prefix::from(first), other, with_https)
|
||||
} else { // fallback to acct:
|
||||
resolve_with_prefix(Prefix::Acct, first, with_https)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that occured while handling an incoming WebFinger request.
|
||||
|
@ -103,7 +149,7 @@ pub enum ResolverError {
|
|||
InvalidResource,
|
||||
|
||||
/// The website of the resource is not the current one.
|
||||
WrongInstance,
|
||||
WrongDomain,
|
||||
|
||||
/// The requested resource was not found.
|
||||
NotFound
|
||||
|
@ -123,36 +169,22 @@ pub trait Resolver<R> {
|
|||
/// (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>;
|
||||
fn find(prefix: Prefix, 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> {
|
||||
fn endpoint(resource: impl Into<String>, 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
68
src/tests.rs
Normal file → Executable file
68
src/tests.rs
Normal file → Executable file
|
@ -2,9 +2,64 @@ use serde_json;
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_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")));
|
||||
assert_eq!(url_for_acct("test", true), Err(WebfingerError::ParseError))
|
||||
fn test_url_for() {
|
||||
assert_eq!(
|
||||
url_for(Prefix::Acct, "test@example.org", true),
|
||||
Ok(String::from("https://example.org/.well-known/webfinger?resource=acct:test@example.org"))
|
||||
);
|
||||
assert_eq!(
|
||||
url_for(Prefix::Acct, "test", true),
|
||||
Err(WebfingerError::ParseError)
|
||||
);
|
||||
assert_eq!(
|
||||
url_for(Prefix::Acct, "test@example.org", false),
|
||||
Ok(String::from("http://example.org/.well-known/webfinger?resource=acct:test@example.org"))
|
||||
);
|
||||
assert_eq!(
|
||||
url_for(Prefix::Group, "test@example.org", true),
|
||||
Ok(String::from("https://example.org/.well-known/webfinger?resource=group:test@example.org"))
|
||||
);
|
||||
assert_eq!(
|
||||
url_for(Prefix::Custom("hey".into()), "test@example.org", true),
|
||||
Ok(String::from("https://example.org/.well-known/webfinger?resource=hey:test@example.org"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve() {
|
||||
let m = mockito::mock("GET", mockito::Matcher::Any)
|
||||
.with_body(r#"
|
||||
{
|
||||
"subject": "acct:test@example.org",
|
||||
"aliases": [
|
||||
"https://example.org/@test/"
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"href": "https://example.org/@test/"
|
||||
},
|
||||
{
|
||||
"rel": "http://schemas.google.com/g/2010#updates-from",
|
||||
"type": "application/atom+xml",
|
||||
"href": "https://example.org/@test/feed.atom"
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "https://example.org/@test/"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#)
|
||||
.create();
|
||||
|
||||
let url = format!("test@{}", mockito::server_url()).replace("http://", "");
|
||||
println!("{}", url);
|
||||
let res = resolve(url, false).unwrap();
|
||||
assert_eq!(res.subject, String::from("acct:test@example.org"));
|
||||
|
||||
m.assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -66,8 +121,8 @@ impl Resolver<&'static str> for MyResolver {
|
|||
"instance.tld"
|
||||
}
|
||||
|
||||
fn find(acct: String, resource_repo: &'static str) -> Result<Webfinger, ResolverError> {
|
||||
if acct == resource_repo.to_string() {
|
||||
fn find(prefix: Prefix, acct: String, resource_repo: &'static str) -> Result<Webfinger, ResolverError> {
|
||||
if acct == resource_repo.to_string() && prefix == Prefix::Acct {
|
||||
Ok(Webfinger {
|
||||
subject: acct.clone(),
|
||||
aliases: vec![acct.clone()],
|
||||
|
@ -90,8 +145,9 @@ impl Resolver<&'static str> for MyResolver {
|
|||
fn test_my_resolver() {
|
||||
assert!(MyResolver::endpoint("acct:admin@instance.tld", "admin").is_ok());
|
||||
assert_eq!(MyResolver::endpoint("acct:test@instance.tld", "admin"), Err(ResolverError::NotFound));
|
||||
assert_eq!(MyResolver::endpoint("acct:admin@oops.ie", "admin"), Err(ResolverError::WrongInstance));
|
||||
assert_eq!(MyResolver::endpoint("acct:admin@oops.ie", "admin"), Err(ResolverError::WrongDomain));
|
||||
assert_eq!(MyResolver::endpoint("admin@instance.tld", "admin"), Err(ResolverError::InvalidResource));
|
||||
assert_eq!(MyResolver::endpoint("admin", "admin"), Err(ResolverError::InvalidResource));
|
||||
assert_eq!(MyResolver::endpoint("acct:admin", "admin"), Err(ResolverError::InvalidResource));
|
||||
assert_eq!(MyResolver::endpoint("group:admin@instance.tld", "admin"), Err(ResolverError::NotFound));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue