Compare commits

...

2 commits

Author SHA1 Message Date
8635755856 Merge pull request 'feat: nodeinfo handler' (#75) from nodeinfo into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #75
2024-09-03 14:31:19 +05:30
49a779e943
feat: nodeinfo handler
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/pull_request_closed/woodpecker Pipeline was successful
2024-09-03 13:55:39 +05:30
3 changed files with 620 additions and 0 deletions

View file

@ -0,0 +1,428 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::HttpResponse;
use actix_web::{get, web, HttpRequest, Responder};
#[cfg(not(test))]
use log::{error, info};
#[cfg(test)]
use println as info;
#[cfg(test)]
use println as error;
use serde::{Deserialize, Serialize};
use url::Url;
use super::types;
use super::{person, repository};
use crate::federation::adapter::input::web::errors::WebError;
use crate::federation::adapter::input::web::WebJsonRepsonse;
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(handler);
}
#[get("/.well-known/host-meta")]
#[tracing::instrument(name = "host meta handler", skip(settings))]
async fn handler(settings: crate::WebSettings) -> WebJsonRepsonse<impl Responder> {
let resp = format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<XRD xmlns=\"http://docs.oasis-open.org/ns/xri/xrd-1.0\">
<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\"https://{}/.well-known/webfinger?resource={{uri}}\"></Link></XRD>", settings.server.domain,
);
Ok(HttpResponse::Ok()
.content_type("application/xrd+xml")
.body(resp))
}
//
//#[cfg(test)]
//pub mod tests {
// use std::sync::Arc;
//
// use super::*;
// use activitypub_federation::fetch::webfinger::{Webfinger, WebfingerLink};
// use actix_identity::IdentityMiddleware;
// use actix_session::{storage::CookieSessionStore, SessionMiddleware};
// use actix_web::http::{header, StatusCode};
// use actix_web::{cookie::Key, http::header::ContentType, test, App};
//
// use crate::federation::adapter::input::web::routes::RoutesRepository;
// use crate::federation::application::port::out::db::get_repository::mock_get_repository;
// use crate::federation::application::port::out::db::save_repository::mock_save_repository;
// use crate::federation::application::port::out::forge::get_repository::mock_forge_get_repository;
// use crate::federation::domain::{Person, Repository};
// use crate::federation::{
// adapter::out::forge::forge_factory::{
// FederationForgeAdapterFactoryInterface, MockFederationForgeAdapterFactoryInterface,
// },
// application::port::out::{
// db::{get_person::*, save_person::*},
// forge::{get_person::*, parse_repository_from_url::*, parse_username_from_url::*},
// },
// };
// use crate::settings::Settings;
// use crate::tests::bdd::*;
// use crate::utils::absolute_url::absolute_url;
// use crate::utils::forges::forge_repository::MockForgeRepositoryInterface;
// use crate::utils::forges::SupportedForges;
//
// pub fn configure_forge_url(s: &mut Settings) {
// let p = Person::default();
// let mut forge_url = p.html_url().clone();
// forge_url.set_path("");
//
// s.forges.forgejo.url = forge_url.clone();
// s.forges.github.url = forge_url.clone();
// }
//
// fn configure_forges() -> types::WebFederationForgeRepositoryInterface {
// let mut mock_forge_factory = MockFederationForgeAdapterFactoryInterface::default();
// mock_forge_factory
// .expect_get_person_adapter()
// .returning(|| mock_forge_get_person(IGNORE_CALL_COUNT));
//
// mock_forge_factory
// .expect_get_parse_username_from_url()
// .returning(|| mock_forge_parse_username_from_url(IGNORE_CALL_COUNT));
//
// mock_forge_factory
// .expect_get_repository()
// .returning(|| mock_forge_get_repository(IGNORE_CALL_COUNT));
//
// mock_forge_factory
// .expect_get_parse_repository_from_url()
// .returning(|| mock_forge_parse_repository_from_url(IGNORE_CALL_COUNT));
//
// let mock_forge_factory: Arc<dyn FederationForgeAdapterFactoryInterface> =
// Arc::new(mock_forge_factory);
//
// let mut mock_forges = MockForgeRepositoryInterface::default();
// mock_forges
// .expect_get_forge_factory()
// .returning(move |_| Some(mock_forge_factory.clone()));
//
// types::WebFederationForgeRepositoryInterface::new(Arc::new(mock_forges))
// }
//
// macro_rules! init_webfinger_app {
// ($s:ident) => {
// test::init_service(
// App::new()
// .wrap(IdentityMiddleware::default())
// .wrap(SessionMiddleware::new(
// CookieSessionStore::default(),
// Key::from($s.server.cookie_secret.as_bytes()),
// ))
// .wrap(tracing_actix_web::TracingLogger::default())
// .app_data(types::WebFederationOutDBGetPersonObj::new(
// mock_get_person_from_preferred_username(IGNORE_CALL_COUNT),
// ))
// .app_data(types::WebFederationOutDBSavePersonObj::new(
// mock_save_person(IGNORE_CALL_COUNT),
// ))
// .app_data(types::WebFederationOutDBGetRepositoryObj::new(
// mock_get_repository(IGNORE_CALL_COUNT),
// ))
// .app_data(types::WebFederationOutDBSaveRepositoryObj::new(
// mock_save_repository(IGNORE_CALL_COUNT),
// ))
// .app_data(configure_forges())
// .app_data(web::Data::new(Arc::new(RoutesRepository::default())))
// .app_data(web::Data::new($s.clone()))
// .service(handler),
// )
// .await
// };
// }
//
// #[actix_web::test]
// async fn test_webfinger_hander_person_web_query() {
// let mut settings = Settings::new().unwrap();
// configure_forge_url(&mut settings);
//
// let s = settings.clone();
// let app = init_webfinger_app!(s);
// let routes = RoutesRepository::default();
//
// let path = {
// let p = Person::default();
// routes.webfinger(p.html_url())
// };
//
// let req = test::TestRequest::get().uri(&path).to_request();
// let resp = test::call_service(&app, req).await;
// let status = resp.status();
// assert_eq!(status, StatusCode::OK);
//
// assert_eq!(
// resp.headers().get("Content-Type").unwrap(),
// "application/jrd+json"
// );
//
// let data: Webfinger = test::read_body_json(resp).await;
// let person = Person::default();
//
// assert_eq!(data.subject, person.html_url().to_string());
// println!("{:#?}", data.links);
// assert_eq!(data.links.len(), 3);
//
// let profile_photo_link = data
// .links
// .iter()
// .find(|l| l.rel == Some("http://webfinger.net/rel/avatar".into()))
// .unwrap();
// assert_eq!(
// profile_photo_link.href,
// person.profile_photo().as_ref().map(|u| u.clone())
// );
//
// let profile_page_link = data
// .links
// .iter()
// .find(|l| l.rel == Some("http://webfinger.net/rel/profile-page".into()))
// .unwrap();
// let expected_profile_page_link =
// routes
// .person
// .actor(person.username(), SupportedForges::Forgejo, &settings);
//
// assert_eq!(
// profile_page_link.href.as_ref().unwrap().path(),
// // any SupportedForges should work, since all hostnames are == example.com in tests
// expected_profile_page_link
// );
// }
//
// #[actix_web::test]
// async fn test_webfinger_hander_person_web_query_forgeflux_hostname() {
// let mut settings = Settings::new().unwrap();
// configure_forge_url(&mut settings);
//
// let s = settings.clone();
// let app = init_webfinger_app!(s);
// let routes = RoutesRepository::default();
// let p = Person::default();
//
// // test webfinger lookup with hostname = settings.server.domain
// let path = {
// let p = Person::default();
// let u = absolute_url(
// &settings,
// &routes
// .person
// .actor(&p.username(), SupportedForges::Forgejo, &settings),
// )
// .unwrap();
//
// routes.webfinger(&u)
// };
// let req = test::TestRequest::get().uri(&path).to_request();
// let resp = test::call_service(&app, req).await;
// let status = resp.status();
// assert_eq!(status, StatusCode::OK);
// let x: Webfinger = test::read_body_json(resp).await;
// assert_eq!(
// x.subject,
// crate::utils::absolute_url::absolute_url(
// &settings,
// &routes
// .person
// .actor(p.username(), SupportedForges::Forgejo, &settings)
// )
// .unwrap()
// .to_string()
// );
// }
//
// #[actix_web::test]
// async fn test_webfinger_hander_person_acct_query() {
// let mut settings = Settings::new().unwrap();
// configure_forge_url(&mut settings);
//
// let s = settings.clone();
// let app = init_webfinger_app!(s);
// let routes = RoutesRepository::default();
//
// let webfinger_resource = {
// Url::parse(&format!(
// "acct:{}@{}",
// Person::default().preferred_username(),
// &settings.server.domain,
// ))
// .unwrap()
// };
//
// println!("{}", webfinger_resource.to_string());
// let path = routes.webfinger(&webfinger_resource);
// let req = test::TestRequest::get().uri(&path).to_request();
// let resp = test::call_service(&app, req).await;
// let status = resp.status();
// assert_eq!(status, StatusCode::OK);
// let x: Webfinger = test::read_body_json(resp).await;
// assert_eq!(x.subject, webfinger_resource.to_string());
//
// let profile_page_link = x
// .links
// .iter()
// .find(|l| l.rel == Some("http://webfinger.net/rel/profile-page".into()))
// .unwrap();
// let expected_profile_page_link = routes.person.actor(
// Person::default().username(),
// SupportedForges::Forgejo,
// &settings,
// );
//
// assert_eq!(
// profile_page_link.href.as_ref().unwrap().path(),
// // any SupportedForges should work, since all hostnames are == example.com in tests
// expected_profile_page_link
// );
// }
//
// #[actix_web::test]
// async fn test_webfinger_hander_repository_acct_query() {
// let mut settings = Settings::new().unwrap();
// configure_forge_url(&mut settings);
//
// let s = settings.clone();
// let app = init_webfinger_app!(s);
// let routes = RoutesRepository::default();
// let repo = Repository::default();
//
// let webfinger_resource = {
// Url::parse(&format!(
// "acct:{}@{}",
// repo.preferred_username(),
// &settings.server.domain,
// ))
// .unwrap()
// };
//
// println!("{}", webfinger_resource.to_string());
// let path = routes.webfinger(&webfinger_resource);
// let req = test::TestRequest::get().uri(&path).to_request();
// let resp = test::call_service(&app, req).await;
// let status = resp.status();
// assert_eq!(status, StatusCode::OK);
// let x: Webfinger = test::read_body_json(resp).await;
// assert_eq!(x.subject, webfinger_resource.to_string());
//
// let profile_page_link = x
// .links
// .iter()
// .find(|l| l.rel == Some("http://webfinger.net/rel/profile-page".into()))
// .unwrap();
// let expected_profile_page_link = routes.repository.actor(
// repo.owner(),
// repo.name(),
// SupportedForges::Forgejo,
// &settings,
// );
//
// assert_eq!(
// profile_page_link.href.as_ref().unwrap().path(),
// // any SupportedForges should work, since all hostnames are == example.com in tests
// expected_profile_page_link
// );
// }
//
// #[actix_web::test]
// async fn test_webfinger_hander_repository_web_query() {
// let mut settings = Settings::new().unwrap();
// configure_forge_url(&mut settings);
//
// let s = settings.clone();
// let app = init_webfinger_app!(s);
// let routes = RoutesRepository::default();
//
// let repository = Repository::default();
//
// let path = { routes.webfinger(repository.html_url()) };
//
// let req = test::TestRequest::get().uri(&path).to_request();
// let resp = test::call_service(&app, req).await;
// let status = resp.status();
// assert_eq!(status, StatusCode::OK);
//
// assert_eq!(
// resp.headers().get("Content-Type").unwrap(),
// "application/jrd+json"
// );
//
// let data: Webfinger = test::read_body_json(resp).await;
//
// assert_eq!(data.subject, repository.html_url().to_string());
// println!("{:#?}", data.links);
// assert_eq!(data.links.len(), 2);
//
// assert!(data
// .links
// .iter()
// .find(|l| l.rel == Some("http://webfinger.net/rel/avatar".into()))
// .is_none());
//
// let profile_page_link = data
// .links
// .iter()
// .find(|l| l.rel == Some("http://webfinger.net/rel/profile-page".into()))
// .unwrap();
// let expected_profile_page_link = routes.repository.actor(
// repository.owner(),
// repository.name(),
// SupportedForges::Forgejo,
// &settings,
// );
//
// assert_eq!(
// profile_page_link.href.as_ref().unwrap().path(),
// // any SupportedForges should work, since all hostnames are == example.com in tests
// expected_profile_page_link
// );
// }
//
// #[actix_web::test]
// async fn test_webfinger_hander_repository_web_query_forgeflux_hostname() {
// let mut settings = Settings::new().unwrap();
// configure_forge_url(&mut settings);
//
// let s = settings.clone();
// let app = init_webfinger_app!(s);
// let routes = RoutesRepository::default();
// let repository = Repository::default();
//
// // test webfinger lookup with hostname = settings.server.domain
// let path = {
// let u = absolute_url(
// &settings,
// &routes.repository.actor(
// repository.owner(),
// repository.name(),
// SupportedForges::Forgejo,
// &settings,
// ),
// )
// .unwrap();
//
// routes.webfinger(&u)
// };
// let req = test::TestRequest::get().uri(&path).to_request();
// let resp = test::call_service(&app, req).await;
// let status = resp.status();
// assert_eq!(status, StatusCode::OK);
// let x: Webfinger = test::read_body_json(resp).await;
// assert_eq!(
// x.subject,
// crate::utils::absolute_url::absolute_url(
// &settings,
// &routes.repository.actor(
// repository.owner(),
// repository.name(),
// SupportedForges::Forgejo,
// &settings
// )
// )
// .unwrap()
// .to_string()
// );
// }
//}

View file

@ -7,6 +7,7 @@ use std::sync::Arc;
use actix_web::web;
mod errors;
mod nodeinfo;
mod person;
mod repository;
mod routes;
@ -22,6 +23,7 @@ pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) {
let f = move |cfg: &mut web::ServiceConfig| {
cfg.app_data(routes);
cfg.configure(webfinger::services);
cfg.configure(nodeinfo::services);
cfg.configure(repository::services);
cfg.configure(person::services);
};

View file

@ -0,0 +1,190 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use actix_web::HttpResponse;
use actix_web::{get, web, HttpRequest, Responder};
#[cfg(not(test))]
use log::{error, info};
#[cfg(test)]
use println as info;
#[cfg(test)]
use println as error;
use serde::{Deserialize, Serialize};
use url::Url;
//use super::types;
use crate::federation::adapter::input::web::WebJsonRepsonse;
use crate::utils::absolute_url;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NodeinfoMeta {
links: Vec<Link>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Link {
rel: Url,
href: Url,
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(list_nodeinfos);
cfg.service(nodeinfo);
}
#[get("/.well-known/nodeinfo")]
#[tracing::instrument(name = "nodeinfo well-known route", skip(settings))]
async fn list_nodeinfos(settings: crate::WebSettings) -> WebJsonRepsonse<impl Responder> {
println!("entry");
let resp = NodeinfoMeta {
links: vec![Link {
rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0").unwrap(),
href: absolute_url::absolute_url(&settings, "/nodeinfo/2.0").unwrap(),
}],
};
println!("all good");
Ok(HttpResponse::Ok().json(resp))
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Nodeinfo {
version: String,
software: Software,
protocols: Vec<String>,
services: Services,
open_registrations: bool,
metadata: HashMap<String, serde_json::Value>,
usage: Usage,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Software {
name: String,
version: String,
}
impl Default for Software {
fn default() -> Self {
Self {
name: "forgeflux".to_string(),
version: "0.0.0".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq)]
pub struct Services {
inbound: Vec<String>,
outbound: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Usage {
users: Users,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Users {
total: usize,
}
#[get("/nodeinfo/2.0")]
#[tracing::instrument(
name = "nodeinfo handler",
// skip(settings)
)]
async fn nodeinfo(// settings: crate::WebSettings,
) -> WebJsonRepsonse<impl Responder> {
let resp = Nodeinfo {
version: "2.0".to_string(),
software: Software::default(),
protocols: vec!["activitypub".to_string(), "forgefed".to_string()],
services: Services::default(),
usage: Usage {
users: Users { total: 0 },
},
open_registrations: false,
metadata: HashMap::default(),
};
Ok(HttpResponse::Ok().json(resp))
}
#[cfg(test)]
pub mod tests {
use super::*;
use actix_identity::IdentityMiddleware;
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::http::StatusCode;
use actix_web::{cookie::Key, test, App};
use crate::settings::Settings;
macro_rules! init_nodeinfo_app {
($s:ident) => {
test::init_service(
App::new()
.wrap(IdentityMiddleware::default())
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
Key::from($s.server.cookie_secret.as_bytes()),
))
.wrap(tracing_actix_web::TracingLogger::default())
.app_data(web::Data::new($s.clone()))
.configure(services),
)
.await
};
}
#[actix_web::test]
async fn test_nodeinfo_list() {
let settings = Settings::new().unwrap();
let s = settings.clone();
let app = init_nodeinfo_app!(s);
let req = test::TestRequest::get()
.uri("/.well-known/nodeinfo")
.to_request();
let resp = test::call_service(&app, req).await;
let status = resp.status();
assert_eq!(status, StatusCode::OK);
let data: NodeinfoMeta = test::read_body_json(resp).await;
assert_eq!(data.links.len(), 1);
let link = data.links.get(0).unwrap();
assert_eq!(
link.rel,
Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0").unwrap()
);
assert_eq!(
link.href,
absolute_url::absolute_url(&settings, "/nodeinfo/2.0").unwrap()
);
}
#[actix_web::test]
async fn test_nodeinfo() {
let settings = Settings::new().unwrap();
let s = settings.clone();
let app = init_nodeinfo_app!(s);
let req = test::TestRequest::get().uri("/nodeinfo/2.0").to_request();
let resp = test::call_service(&app, req).await;
let status = resp.status();
assert_eq!(status, StatusCode::OK);
let data: Nodeinfo = test::read_body_json(resp).await;
assert_eq!(data.version, "2.0");
assert_eq!(data.software.name, "forgeflux");
assert!(data.protocols.iter().any(|p| p == "activitypub"));
assert!(data.protocols.iter().any(|p| p == "forgefed"));
}
}