Add resolver
This commit is contained in:
parent
3358daee48
commit
a058f2da30
|
@ -10,11 +10,9 @@ fn main() {
|
||||||
let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build();
|
let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build();
|
||||||
let conn = ClientConnector::with_connector(ssl_conn).start();
|
let conn = ClientConnector::with_connector(ssl_conn).start();
|
||||||
|
|
||||||
let fut = Webfinger::fetch(conn, "Sir_Boops@sergal.org", "mastodon.sergal.org")
|
let fut = Webfinger::fetch(conn, "asonix@asonix.dog", "localhost:8000", false)
|
||||||
.map(move |w: Webfinger| {
|
.map(move |w: Webfinger| {
|
||||||
if let Some(ref link) = w.activitypub() {
|
println!("asonix's webfinger:\n{:#?}", w);
|
||||||
println!("Sir Boop's activitypub: {:#?}", link);
|
|
||||||
}
|
|
||||||
|
|
||||||
System::current().stop();
|
System::current().stop();
|
||||||
})
|
})
|
42
examples/resolver.rs
Normal file
42
examples/resolver.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use actix_web::{server, App};
|
||||||
|
use actix_webfinger::{Resolver, Webfinger};
|
||||||
|
use futures::{future::IntoFuture, Future};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MyState {
|
||||||
|
domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MyResolver;
|
||||||
|
|
||||||
|
impl Resolver<MyState> for MyResolver {
|
||||||
|
type Error = actix_web::error::JsonPayloadError;
|
||||||
|
|
||||||
|
fn find(
|
||||||
|
account: &str,
|
||||||
|
domain: &str,
|
||||||
|
state: &MyState,
|
||||||
|
) -> Box<dyn Future<Item = Option<Webfinger>, Error = Self::Error>> {
|
||||||
|
let w = if domain == state.domain {
|
||||||
|
Some(Webfinger::new(&format!("{}@{}", account, domain)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Box::new(Ok(w).into_future())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
server::new(|| {
|
||||||
|
App::with_state(MyState {
|
||||||
|
domain: "asonix.dog".to_owned(),
|
||||||
|
})
|
||||||
|
.resource("/.well-known/webfinger", |r| {
|
||||||
|
r.with(<MyResolver as Resolver<MyState>>::endpoint)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.bind("127.0.0.1:8000")
|
||||||
|
.unwrap()
|
||||||
|
.run();
|
||||||
|
}
|
195
src/lib.rs
195
src/lib.rs
|
@ -1,11 +1,79 @@
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
client::{self, ClientConnector},
|
client::{self, ClientConnector},
|
||||||
HttpMessage,
|
error::ResponseError,
|
||||||
|
HttpMessage, HttpResponse, Query, State,
|
||||||
};
|
};
|
||||||
|
use failure::Fail;
|
||||||
use futures::{future::IntoFuture, Future};
|
use futures::{future::IntoFuture, Future};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Fail)]
|
||||||
|
#[fail(display = "Resource {} is invalid", _0)]
|
||||||
|
pub struct InvalidResource(String);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WebfingerResource {
|
||||||
|
account: String,
|
||||||
|
domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for WebfingerResource {
|
||||||
|
type Err = InvalidResource;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let trimmed = s.trim_start_matches("acct:").trim_start_matches('@');
|
||||||
|
|
||||||
|
if let Some(index) = trimmed.find('@') {
|
||||||
|
let (account, domain) = trimmed.split_at(index);
|
||||||
|
|
||||||
|
Ok(WebfingerResource {
|
||||||
|
account: account.to_owned(),
|
||||||
|
domain: domain.trim_start_matches('@').to_owned(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(InvalidResource(s.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::de::Deserialize<'de> for WebfingerResource {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
s.parse::<WebfingerResource>()
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct WebfingerQuery {
|
||||||
|
resource: WebfingerResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Resolver<S> {
|
||||||
|
type Error: ResponseError;
|
||||||
|
|
||||||
|
fn find(
|
||||||
|
account: &str,
|
||||||
|
domain: &str,
|
||||||
|
state: &S,
|
||||||
|
) -> Box<dyn Future<Item = Option<Webfinger>, Error = Self::Error>>;
|
||||||
|
|
||||||
|
fn endpoint(
|
||||||
|
(query, state): (Query<WebfingerQuery>, State<S>),
|
||||||
|
) -> Box<dyn Future<Item = HttpResponse, Error = Self::Error>> {
|
||||||
|
let WebfingerResource { account, domain } = query.into_inner().resource;
|
||||||
|
|
||||||
|
Box::new(Self::find(&account, &domain, &state).map(|w| match w {
|
||||||
|
Some(w) => w.respond(),
|
||||||
|
None => HttpResponse::NotFound().finish(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct Link {
|
pub struct Link {
|
||||||
pub rel: String,
|
pub rel: String,
|
||||||
|
@ -27,14 +95,47 @@ pub struct Webfinger {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Webfinger {
|
impl Webfinger {
|
||||||
|
pub fn new(subject: &str) -> Self {
|
||||||
|
Webfinger {
|
||||||
|
aliases: Vec::new(),
|
||||||
|
links: Vec::new(),
|
||||||
|
subject: subject.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_aliases(&mut self, aliases: &[String]) -> &mut Self {
|
||||||
|
self.aliases.extend_from_slice(aliases);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_alias(&mut self, alias: &str) -> &mut Self {
|
||||||
|
self.aliases.push(alias.to_owned());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn aliases(&self) -> &[String] {
|
pub fn aliases(&self) -> &[String] {
|
||||||
&self.aliases
|
&self.aliases
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_link(&mut self, link: Link) -> &mut Self {
|
||||||
|
self.links.push(link);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn links(&self) -> &[Link] {
|
pub fn links(&self) -> &[Link] {
|
||||||
&self.links
|
&self.links
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_activitypub(&mut self, href: &str) -> &mut Self {
|
||||||
|
self.links.push(Link {
|
||||||
|
rel: "self".to_owned(),
|
||||||
|
kind: Some("application/activity+json".to_owned()),
|
||||||
|
href: Some(href.to_owned()),
|
||||||
|
template: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn activitypub(&self) -> Option<&Link> {
|
pub fn activitypub(&self) -> Option<&Link> {
|
||||||
self.links.iter().find(|l| {
|
self.links.iter().find(|l| {
|
||||||
l.rel == "self"
|
l.rel == "self"
|
||||||
|
@ -45,39 +146,104 @@ impl Webfinger {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_profile(&mut self, href: &str) -> &mut Self {
|
||||||
|
self.links.push(Link {
|
||||||
|
rel: "http://webfinger.net/rel/profile-page".to_owned(),
|
||||||
|
href: Some(href.to_owned()),
|
||||||
|
kind: Some("text/html".to_owned()),
|
||||||
|
template: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn profile(&self) -> Option<&Link> {
|
pub fn profile(&self) -> Option<&Link> {
|
||||||
self.links
|
self.links
|
||||||
.iter()
|
.iter()
|
||||||
.find(|l| l.rel == "http://webfinger.net/rel/profile-page")
|
.find(|l| l.rel == "http://webfinger.net/rel/profile-page")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_atom(&mut self, href: &str) -> &mut Self {
|
||||||
|
self.links.push(Link {
|
||||||
|
rel: "http://schemas.google.com/g/2010#updates-from".to_owned(),
|
||||||
|
href: Some(href.to_owned()),
|
||||||
|
kind: Some("application/atom+xml".to_owned()),
|
||||||
|
template: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn atom(&self) -> Option<&Link> {
|
pub fn atom(&self) -> Option<&Link> {
|
||||||
self.links
|
self.links
|
||||||
.iter()
|
.iter()
|
||||||
.find(|l| l.rel == "http://schemas.google.com/g/2010#updates-from")
|
.find(|l| l.rel == "http://schemas.google.com/g/2010#updates-from")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_salmon(&mut self, href: &str) -> &mut Self {
|
||||||
|
self.links.push(Link {
|
||||||
|
rel: "salmon".to_owned(),
|
||||||
|
href: Some(href.to_owned()),
|
||||||
|
kind: None,
|
||||||
|
template: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn salmon(&self) -> Option<&Link> {
|
pub fn salmon(&self) -> Option<&Link> {
|
||||||
self.links.iter().find(|l| l.rel == "salmon")
|
self.links.iter().find(|l| l.rel == "salmon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_magic_public_key(&mut self, magic_public_key: &str) -> &mut Self {
|
||||||
|
self.links.push(Link {
|
||||||
|
rel: "magic-public-key".to_owned(),
|
||||||
|
href: Some(format!(
|
||||||
|
"data:application/magic-public-key,{}",
|
||||||
|
magic_public_key
|
||||||
|
)),
|
||||||
|
kind: None,
|
||||||
|
template: None,
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn magic_public_key(&self) -> Option<&Link> {
|
pub fn magic_public_key(&self) -> Option<&Link> {
|
||||||
self.links.iter().find(|l| l.rel == "magic-public-key")
|
self.links.iter().find(|l| l.rel == "magic-public-key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_ostatus(&mut self, template: &str) -> &mut Self {
|
||||||
|
self.links.push(Link {
|
||||||
|
rel: "http://ostatus.org/schema/1.0/subscribe".to_owned(),
|
||||||
|
href: None,
|
||||||
|
kind: None,
|
||||||
|
template: Some(template.to_owned()),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ostatus(&self) -> Option<&Link> {
|
pub fn ostatus(&self) -> Option<&Link> {
|
||||||
self.links
|
self.links
|
||||||
.iter()
|
.iter()
|
||||||
.find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe")
|
.find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn respond(self) -> HttpResponse {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/jrd+json")
|
||||||
|
.json(self)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fetch(
|
pub fn fetch(
|
||||||
conn: Addr<ClientConnector>,
|
conn: Addr<ClientConnector>,
|
||||||
user: &str,
|
user: &str,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
) -> Box<Future<Item = Self, Error = actix_web::Error>> {
|
https: bool,
|
||||||
|
) -> Box<dyn Future<Item = Self, Error = actix_web::Error>> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://{}/.well-known/webfinger?resource=acct:{}",
|
"{}://{}/.well-known/webfinger?resource=acct:{}",
|
||||||
|
if https {
|
||||||
|
"https"
|
||||||
|
} else {
|
||||||
|
"http"
|
||||||
|
},
|
||||||
domain, user
|
domain, user
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -98,10 +264,17 @@ impl Webfinger {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::Webfinger;
|
use crate::{Webfinger, WebfingerQuery};
|
||||||
|
|
||||||
const SIR_BOOPS: &'static str = r#"{"subject":"acct:Sir_Boops@sergal.org","aliases":["https://mastodon.sergal.org/@Sir_Boops","https://mastodon.sergal.org/users/Sir_Boops"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://mastodon.sergal.org/@Sir_Boops"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://mastodon.sergal.org/users/Sir_Boops.atom"},{"rel":"self","type":"application/activity+json","href":"https://mastodon.sergal.org/users/Sir_Boops"},{"rel":"salmon","href":"https://mastodon.sergal.org/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.vwDujxmxoYHs64MyVB3LG5ZyBxV3ufaMRBFu42bkcTpISq1WwZ-3Zb6CI8zOO-nM-Q2llrVRYjZa4ZFnOLvMTq_Kf-Zf5wy2aCRer88gX-MsJOAtItSi412y0a_rKOuFaDYLOLeTkRvmGLgZWbsrZJOp-YWb3zQ5qsIOInkc5BwI172tMsGeFtsnbNApPV4lrmtTGaJ8RiM8MR7XANBOfOHggSt1-eAIKGIsCmINEMzs1mG9D75xKtC_sM8GfbvBclQcBstGkHAEj1VHPW0ch6Bok5_QQppicyb8UA1PAA9bznSFtKlYE4xCH8rlCDSDTBRtdnBWHKcj619Ujz4Qaw==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://mastodon.sergal.org/authorize_interaction?uri={uri}"}]}"#;
|
const SIR_BOOPS: &'static str = r#"{"subject":"acct:Sir_Boops@sergal.org","aliases":["https://mastodon.sergal.org/@Sir_Boops","https://mastodon.sergal.org/users/Sir_Boops"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://mastodon.sergal.org/@Sir_Boops"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://mastodon.sergal.org/users/Sir_Boops.atom"},{"rel":"self","type":"application/activity+json","href":"https://mastodon.sergal.org/users/Sir_Boops"},{"rel":"salmon","href":"https://mastodon.sergal.org/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.vwDujxmxoYHs64MyVB3LG5ZyBxV3ufaMRBFu42bkcTpISq1WwZ-3Zb6CI8zOO-nM-Q2llrVRYjZa4ZFnOLvMTq_Kf-Zf5wy2aCRer88gX-MsJOAtItSi412y0a_rKOuFaDYLOLeTkRvmGLgZWbsrZJOp-YWb3zQ5qsIOInkc5BwI172tMsGeFtsnbNApPV4lrmtTGaJ8RiM8MR7XANBOfOHggSt1-eAIKGIsCmINEMzs1mG9D75xKtC_sM8GfbvBclQcBstGkHAEj1VHPW0ch6Bok5_QQppicyb8UA1PAA9bznSFtKlYE4xCH8rlCDSDTBRtdnBWHKcj619Ujz4Qaw==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://mastodon.sergal.org/authorize_interaction?uri={uri}"}]}"#;
|
||||||
|
|
||||||
|
const QUERIES: [&'static str; 4] = [
|
||||||
|
r#"{"resource":"acct:asonix@asonix.dog"}"#,
|
||||||
|
r#"{"resource":"asonix@asonix.dog"}"#,
|
||||||
|
r#"{"resource":"acct:@asonix@asonix.dog"}"#,
|
||||||
|
r#"{"resource":"@asonix@asonix.dog"}"#,
|
||||||
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_deserialize_sir_boops() {
|
fn can_deserialize_sir_boops() {
|
||||||
let webfinger: Result<Webfinger, _> = serde_json::from_str(SIR_BOOPS);
|
let webfinger: Result<Webfinger, _> = serde_json::from_str(SIR_BOOPS);
|
||||||
|
@ -117,4 +290,18 @@ mod tests {
|
||||||
assert!(webfinger.magic_public_key().is_some());
|
assert!(webfinger.magic_public_key().is_some());
|
||||||
assert!(webfinger.profile().is_some());
|
assert!(webfinger.profile().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_deserialize_queries() {
|
||||||
|
for resource in &QUERIES {
|
||||||
|
let res: Result<WebfingerQuery, _> = serde_json::from_str(resource);
|
||||||
|
|
||||||
|
assert!(res.is_ok());
|
||||||
|
|
||||||
|
let query = res.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(query.resource.account, "asonix");
|
||||||
|
assert_eq!(query.resource.domain, "asonix.dog");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue