From 9449d5812ffa7030f5aa7d4faa8557c37fa86b02 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 27 May 2023 18:32:20 +0900 Subject: [PATCH] add actor handler --- Cargo.lock | 1 + Cargo.toml | 1 + src/apub.rs | 3 +++ src/apub/actor.rs | 29 ++++++++++++++++++++++++ src/ctx.rs | 3 +++ src/ctx/config.rs | 34 ++++++++++++++++++++++++++++ src/ctx/db.rs | 30 ++++++++---------------- src/ctx/user.rs | 49 ++++++++++++++++------------------------ src/handler.rs | 1 + src/handler/actor.rs | 47 ++++++++++++++++++++++++++++++++++++++ src/handler/webfinger.rs | 38 ++++++++++++++++++++++--------- src/main.rs | 33 +++++++++++++++++++++++++-- 12 files changed, 207 insertions(+), 62 deletions(-) create mode 100644 src/apub.rs create mode 100644 src/apub/actor.rs create mode 100644 src/ctx/config.rs create mode 100644 src/handler/actor.rs diff --git a/Cargo.lock b/Cargo.lock index b006249..29637d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "regex", "rocket", "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9b762f7..da0ea55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ lazy_static = "1.4.0" regex = "1.8.3" rocket = { version = "=0.5.0-rc.3", features = ["json"] } serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" diff --git a/src/apub.rs b/src/apub.rs new file mode 100644 index 0000000..81b86d2 --- /dev/null +++ b/src/apub.rs @@ -0,0 +1,3 @@ +pub mod actor; + +pub use actor::Actor; diff --git a/src/apub/actor.rs b/src/apub/actor.rs new file mode 100644 index 0000000..7f43ace --- /dev/null +++ b/src/apub/actor.rs @@ -0,0 +1,29 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct Actor { + pub context: Vec, + pub id: String, + #[serde(rename = "type")] + pub type_: String, + pub preferred_username: String, + pub inbox: String, + pub outbox: String, + pub public_key: PublicKey, +} + +#[derive(Serialize, Deserialize)] +pub struct PublicKey { + pub id: String, + pub owner: String, + pub public_key_pem: String, +} + +impl Actor { + pub fn default_context() -> Vec { + vec![ + "https://www.w3.org/ns/activitystreams".to_owned(), + "https://w3id.org/security/v1".to_owned(), + ] + } +} diff --git a/src/ctx.rs b/src/ctx.rs index 9be61f2..0ae67a8 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,8 +1,11 @@ +pub mod config; pub mod db; pub mod user; +pub use config::Config; pub use db::DB; pub struct Ctx { + pub cfg: Config, pub db: Box, } diff --git a/src/ctx/config.rs b/src/ctx/config.rs new file mode 100644 index 0000000..7b42450 --- /dev/null +++ b/src/ctx/config.rs @@ -0,0 +1,34 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub proto: String, + pub domain: String, + pub basepath: String, +} + +impl Config { + pub fn default() -> Config { + Config { + proto: "https".to_owned(), + domain: "localhost".to_owned(), + basepath: "".to_owned(), + } + } + pub fn load(path: &str) -> Result { + match std::fs::read_to_string(path) { + Ok(text) => match serde_json::from_str(&text) { + Ok(cfg) => Ok(cfg), + Err(e) => Err(format!("{}", e)), + }, + Err(e) => Err(format!("{}", e)), + } + } + + pub fn get_baseurl(&self) -> String { + let mut p = + format!("{}://{}/{}", self.proto, self.domain, self.basepath); + while p.ends_with("/") { p.pop(); } + return p; + } +} diff --git a/src/ctx/db.rs b/src/ctx/db.rs index 020c85e..07e855d 100644 --- a/src/ctx/db.rs +++ b/src/ctx/db.rs @@ -5,26 +5,16 @@ pub trait DB : Send + Sync { fn delete(&self, d: &user::Id) -> Result<(), String>; fn exists(&self, d: &user::Id) -> Result; - fn get_profile(&self, id: &user::Id) -> Result; - fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result; + fn get_profile(&self, id: &user::Id) -> Result; + fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result; } -pub struct Null; -impl DB for Null { - fn new(&self, d: &user::Id) -> Result<(), String> { - Err("not implemented".to_owned()) - } - fn delete(&self, d: &user::Id) -> Result<(), String> { - Err("not implemented".to_owned()) - } - fn exists(&self, d: &user::Id) -> Result { - Err("not implemented".to_owned()) - } - - fn get_profile(&self, id: &user::Id) -> Result { - Err("not implemented".to_owned()) - } - fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result { - Err("not implemented".to_owned()) - } +pub enum ReadError { + Missing, + Internal(String), +} +pub enum WriteError { + Missing, + Violation, + Internal(String), } diff --git a/src/ctx/user.rs b/src/ctx/user.rs index 23914dc..2d71720 100644 --- a/src/ctx/user.rs +++ b/src/ctx/user.rs @@ -1,42 +1,33 @@ use lazy_static::lazy_static; use regex::Regex; -pub struct Data { - id: Id, - profile: Option, -} pub struct Id { - name: String, - host: Option, + pub name: String, + pub host: Option, } pub struct Profile { - name: String, -} - - -impl Data { - fn get_profile(&self) -> Result { - Err("not implemented".to_owned()) - } - fn set_profile(&self, d: &Profile) -> Result<(), String> { - Err("not implemented".to_owned()) - } + pub name: String, + pub private: bool, } // hello@world.com -> Id {hello, world.com} -pub fn parse_id(v: &str) -> Result { - lazy_static! { - static ref RE: Regex = Regex::new(r"^([A-Za-z0-9_]+)(?:@([A-Za-z0-9\-\.]+))?$").unwrap(); - } - match RE.captures(v) { - Some(caps) => Ok(Id { - name: caps.get(1).unwrap().as_str().to_owned(), - host: caps.get(2).map_or(None, |m| Some(m.as_str().to_owned())), - }), - None => Err("invalid idstr".to_owned()), - } -} impl Id { + pub fn parse(v: &str) -> Result { + lazy_static! { + static ref RE: Regex = Regex::new(r"^([A-Za-z0-9_]+)(?:@([A-Za-z0-9\-\.]+))?$").unwrap(); + } + match RE.captures(v) { + Some(caps) => Ok(Id { + name: caps.get(1).unwrap().as_str().to_owned(), + host: caps.get(2).map_or(None, |m| Some(m.as_str().to_owned())), + }), + None => Err("invalid idstr".to_owned()), + } + } + pub fn local(name: &str) -> Id { + Id {name: name.to_owned(), host: None} + } + fn to_string(&self, default_host: &str) -> String { format!("{}@{}", self.name, self.host.as_deref().unwrap_or(default_host)) } diff --git a/src/handler.rs b/src/handler.rs index a5a4087..f5f4255 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1 +1,2 @@ +pub mod actor; pub mod webfinger; diff --git a/src/handler/actor.rs b/src/handler/actor.rs new file mode 100644 index 0000000..2547daa --- /dev/null +++ b/src/handler/actor.rs @@ -0,0 +1,47 @@ +use rocket::{get, Responder, State}; +use rocket::serde::json::Json; + +use crate::apub; +use crate::ctx::db; +use crate::ctx::user; + +use crate::ctx::Ctx; + +#[derive(Responder)] +pub enum Response { + #[response(status = 200, content_type = "json")] + Found(Json), + + #[response(status = 404)] + Missing(String), + + #[response(status = 500)] + InternalError(String), +} + +#[get("/")] +pub fn main(ctx: &State, name: &str) -> Response { + match ctx.db.get_profile(&user::Id::local(name)) { + Ok(profile) => Response::Found(Json(new(ctx, name, &profile))), + Err(db::ReadError::Missing) => Response::Missing(format!("unknown actor: {}", name)), + Err(db::ReadError::Internal(msg)) => + Response::InternalError(format!("internal error: {}", msg)), + } +} + +fn new(ctx: &Ctx, id: &str, p: &user::Profile) -> apub::Actor { + let idurl = format!("{}/{}", ctx.cfg.get_baseurl(), id); + apub::Actor { + context: apub::Actor::default_context(), + id : idurl.clone(), + type_ : "Person".to_owned(), + preferred_username: id.to_owned(), + inbox : format!("{}/inbox", idurl), + outbox : format!("{}/outbox", idurl), + public_key: apub::actor::PublicKey { + id: format!("{}#main-key", idurl), + owner: idurl, + public_key_pem: "".to_owned(), + }, + } +} diff --git a/src/handler/webfinger.rs b/src/handler/webfinger.rs index ba731e8..9551f30 100644 --- a/src/handler/webfinger.rs +++ b/src/handler/webfinger.rs @@ -7,12 +7,21 @@ use crate::ctx::Ctx; #[derive(Serialize)] pub struct WebFinger { - subject: String, - aliases: Vec, + subject: String, + aliases: Vec, + links: Vec, +} + +#[derive(Serialize)] +pub struct Link { + rel: String, + #[serde(rename(serialize = "type"))] + type_: String, + href: String, } #[derive(Responder)] -pub enum WebFingerResponse { +pub enum Response { #[response(status = 200, content_type = "json")] Found(Json), @@ -27,22 +36,29 @@ pub enum WebFingerResponse { } #[get("/?")] -pub fn main(ctx: &State, resource: &str) -> WebFingerResponse { +pub fn main(ctx: &State, resource: &str) -> Response { match parse_resource(&resource) { - Ok(("acct", idstr)) | Ok(("", idstr)) => match ctx::user::parse_id(idstr) { + Ok(("acct", idstr)) | Ok(("", idstr)) => match ctx::user::Id::parse(idstr) { Ok(id) => match ctx.db.exists(&id) { - Ok(true) => WebFingerResponse::Found(Json(WebFinger { + Ok(true) => Response::Found(Json(WebFinger { subject: resource.to_owned(), aliases: vec![resource.to_owned()], + links: vec![ + Link { + rel: "self".to_owned(), + type_: "application/activity+json".to_owned(), + href: format!("{}/{}", ctx.cfg.get_baseurl(), id.name), + }, + ], })), - Ok(false) => WebFingerResponse::Missing(format!("unknown resource: {}", resource)), - Err(msg) => WebFingerResponse::InternalError(format!("error: {}", msg)) + Ok(false) => Response::Missing(format!("unknown resource: {}", resource)), + Err(msg) => Response::InternalError(format!("error: {}", msg)) } - Err(msg) => WebFingerResponse::ParseError(format!("error: {}", msg)), + Err(msg) => Response::ParseError(format!("error: {}", msg)), }, - Ok((kind, _)) => WebFingerResponse::ParseError(format!("error: unknown kind, {}", kind)), - Err(msg) => WebFingerResponse::ParseError(format!("error: {}", msg)), + Ok((kind, _)) => Response::ParseError(format!("error: unknown kind, {}", kind)), + Err(msg) => Response::ParseError(format!("error: {}", msg)), } } diff --git a/src/main.rs b/src/main.rs index a2b24e9..4f36987 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,45 @@ +mod apub; mod ctx; mod handler; use rocket; +use ctx::db; +use ctx::user; use ctx::Ctx; +use ctx::Config; #[rocket::launch] fn launch() -> _ { let ctx = Ctx { - db: Box::new(ctx::db::Null {}), + cfg: Config::load("fadaway.json").unwrap_or(Config::default()), + db : Box::new(FakeDB {}), }; rocket::build(). manage(ctx). - mount("/.well-known/webfinger", rocket::routes![handler::webfinger::main]) + mount("/.well-known/webfinger", rocket::routes![handler::webfinger::main]). + mount("/", rocket::routes![handler::actor::main]) +} + +pub struct FakeDB; +impl ctx::DB for FakeDB { + fn new(&self, d: &user::Id) -> Result<(), String> { + Err("not implemented".to_owned()) + } + fn delete(&self, d: &user::Id) -> Result<(), String> { + Err("not implemented".to_owned()) + } + fn exists(&self, d: &user::Id) -> Result { + Ok(true) + } + + fn get_profile(&self, id: &user::Id) -> Result { + Ok(user::Profile { + name: id.name.to_owned(), + private: true, + }) + } + fn set_profile(&self, id: &user::Id, d: user::Profile) -> Result { + Err(db::WriteError::Internal("not implemented".to_owned())) + } }