diff --git a/bin/pupctl/src/main.rs b/bin/pupctl/src/main.rs index 8f62802..85761d9 100644 --- a/bin/pupctl/src/main.rs +++ b/bin/pupctl/src/main.rs @@ -1,59 +1,73 @@ #![feature(iterator_try_collect)] use puppy::{ actor::Actor, - data::{Bite, Profile}, + auth::Verifier, + config::Config, + data::{schema, Bite, Profile}, post::Author, - store::util::IterExt as _, + store::{util::IterExt as _, Store}, Context, }; fn main() -> puppy::Result<()> { // puppy::store::Store::nuke(".state")?; - puppy::context(|cx| { - let db = cx.store(); - println!("creating actors"); - let riley = get_or_create_actor(&cx, "riley")?; - let linen = get_or_create_actor(&cx, "linen")?; - if true { - println!("creating posts"); - puppy::post::create_post(&cx, riley.key, "@linen <3")?; - puppy::post::create_post(&cx, linen.key, "@riley <3")?; - } + let config = Config { + ap_domain: "test.piss-on.me".to_string(), + wf_domain: "test.piss-on.me".to_string(), + state_dir: ".state".to_string(), + port: 1312, + }; + let verifier = Verifier::load(&config); + let db = Store::open(&config.state_dir, schema())?; + let cx = Context::new(config, db.clone(), verifier); + println!("creating actors"); + let riley = get_or_create_actor(&cx, "riley")?; + let linen = get_or_create_actor(&cx, "linen")?; + if true { + println!("creating posts"); + puppy::post::create_post(&cx, riley.key, "@linen <3")?; + puppy::post::create_post(&cx, linen.key, "@riley <3")?; + } - if true { - println!("making riley follow linen"); - if !riley.follows(&cx, &linen)? { + if true { + println!("making riley follow linen"); + + cx.run(|tx| { + if !riley.follows(&tx, &linen)? { println!("follow relation does not exist yet"); if let Some(req) = linen - .pending_requests(&cx) + .pending_requests(&tx) .find_ok(|r| r.origin == riley.key)? { println!("accepting the pending follow request"); - linen.do_accept_request(&cx, req)?; + linen.do_accept_request(&cx, req) } else { println!("no pending follow request; creating"); - riley.do_follow_request(&cx, &linen)?; + riley.do_follow_request(&cx, &linen).map(|_| ()) } } else { println!("riley already follows linen"); + Ok(()) } - } + })?; + } - println!("\nPosts on the instance:"); - for post in puppy::post::fetch_timeline(&db, .., None)?.posts() { - let Author { ref handle, .. } = post.author; - let content = post.content.content.as_ref().unwrap(); - println!("- {:?} by {handle}:\n{content}", post.id) - } + println!("\nPosts on the instance:"); + for post in puppy::post::fetch_timeline(&db, .., None)?.posts() { + let Author { ref handle, .. } = post.author; + let content = post.content.content.as_ref().unwrap(); + println!("- {:?} by {handle}:\n{content}", post.id) + } + cx.run(|tx| { println!("\nLinen's followers:"); - for id in linen.followers(&cx).try_collect::>()? { + for id in linen.followers(&tx).try_collect::>()? { let Profile { account_name, .. } = db.get_mixin(id)?.unwrap(); println!("- @{account_name} ({id})"); } println!("\nRiley's following:"); - for id in riley.following(&cx).try_collect::>()? { + for id in riley.following(&tx).try_collect::>()? { let Profile { account_name, .. } = db.get_mixin(id)?.unwrap(); println!("- @{account_name} ({id})"); } @@ -61,7 +75,7 @@ fn main() -> puppy::Result<()> { if false { println!("Biting riley"); linen.do_bite(&cx, &riley)?; - for Bite { id, biter, .. } in riley.bites_suffered(&cx).try_collect::>()? { + for Bite { id, biter, .. } in riley.bites_suffered(&tx).try_collect::>()? { let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap(); println!("riley was bitten by @{account_name} at {}", id.timestamp()); } @@ -70,8 +84,8 @@ fn main() -> puppy::Result<()> { }) } -fn get_or_create_actor(cx: &Context<'_>, username: &str) -> puppy::Result { - let user = Actor::by_username(cx, username)?; +fn get_or_create_actor(cx: &Context, username: &str) -> puppy::Result { + let user = cx.run(|tx| Actor::by_username(tx, username))?; match user { Some(key) => { println!("found '{username}' ({key:?})"); diff --git a/bin/server/src/api.rs b/bin/server/src/api.rs index c2219bb..8359192 100644 --- a/bin/server/src/api.rs +++ b/bin/server/src/api.rs @@ -11,13 +11,13 @@ pub mod ap { config::Config, data::{Id, PrivateKey, PublicKey}, fetch::{signatures::Private, SigningKey}, - get_local_ap_object, Key, + get_local_ap_object, Context, Key, }; use serde_json::{to_string, Value}; use crate::{respond, Response}; /// Proxy a request through the instance. - pub async fn proxy(params: &[(&str, &str)]) -> Response { + pub async fn proxy(cx: &Context, params: &[(&str, &str)]) -> Response { // Extract our query parameters. let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else { return respond(400, Some("Expected `user` query param"), []); @@ -27,15 +27,16 @@ pub mod ap { }; // Look up the actor's key in the store (which is accessible through the puppy context). - let signing_key = puppy::context::<_, puppy::Error>(|cx| try { - let actor = Actor::by_username(&cx, user)?.unwrap(); + let Ok(signing_key) = cx.run(|tx| { + let actor = Actor::by_username(&tx, user)?.unwrap(); let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) = - cx.store().get_mixin_many(actor.key)?; - let Id(owner) = cx.store().get_alias(actor.key)?.unwrap(); + tx.get_mixin_many(actor.key)?; + let Id(owner) = tx.get_alias(actor.key)?.unwrap(); let inner = Private::decode_pem(&key_pem); - SigningKey { id: key_id, owner, inner } - }) - .unwrap(); + Ok(SigningKey { id: key_id, owner, inner }) + }) else { + panic!("failed to get signing key"); + }; eprintln!("proxy: params: {params:?}"); // Proxy the request through our fetcher. @@ -47,16 +48,16 @@ pub mod ap { } /// Handle POSTs to actor inboxes. Requires request signature. - pub fn inbox(actor_id: &str, sig: Signer, body: Value, cfg: Config) -> Response { + pub fn inbox(cx: &Context, actor_id: &str, sig: Signer, body: Value) -> Response { todo!() } /// Serve an ActivityPub object as json-ld. - pub fn serve_object(object_ulid: &str) -> Response { + pub fn serve_object(cx: &Context, object_ulid: &str) -> Response { let Ok(parsed) = object_ulid.parse::() else { return respond(400, Some("improperly formatted id"), []); }; - let result = puppy::context(|cx| get_local_ap_object(&cx, parsed)); + let result = cx.run(|tx| get_local_ap_object(&tx, parsed)); let Ok(object) = result else { return respond(404, >::None, []); }; @@ -67,8 +68,8 @@ pub mod ap { const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json"); /// Serve the special actor used for signing requests. - pub fn serve_verifier_actor(cfg: Config) -> Response { - let body = Verifier::load(&cfg).to_json_ld(); + pub fn serve_verifier_actor(cx: &Context) -> Response { + let body = cx.verifier().to_json_ld(); let encoded = serde_json::to_vec(&body).unwrap(); respond(200, Some(encoded), [AP_CONTENT_TYPE]) } @@ -77,26 +78,31 @@ pub mod ap { pub mod wf { //! WebFinger endpoints and related stuff. - use puppy::{config::Config, data::Username, Error}; + use puppy::{data::Username, Context, Error}; use serde_json::json; use crate::{respond, Response}; const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json"); - pub fn resolve(query: &[(&str, &str)], cfg: Config) -> Response { + pub fn resolve(cx: &Context, query: &[(&str, &str)]) -> Response { match query.iter().find_map(get_handle) { // Serve JRDs for local actors. - Some(handle) if cfg.wf_domain == handle.instance => { - let id = puppy::context::<_, Error>(|cx| try { + Some(handle) if cx.config().wf_domain == handle.instance => { + let id = { let user = cx .store() - .lookup(Username(handle.username.to_string()))? + .lookup(Username(handle.username.to_string())) + .unwrap() .unwrap(); - let id = cx.store().get_alias::(user)?.unwrap().0; + let id = cx + .store() + .get_alias::(user) + .unwrap() + .unwrap() + .0; id - }) - .unwrap(); + }; let jrd = json!({ "subject": format!("acct:{}@{}", handle.username, handle.instance), "links": [ diff --git a/bin/server/src/main.rs b/bin/server/src/main.rs index 02cf2e5..3d213b1 100644 --- a/bin/server/src/main.rs +++ b/bin/server/src/main.rs @@ -3,16 +3,15 @@ use std::convert::Infallible; use std::net::SocketAddr; -use http::request::Parts; use http_body_util::{BodyExt as _, Full}; use hyper::body::Bytes; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; -use puppy::auth::{SigError, Signer}; -use puppy::{auth::verify_signature, config::Config}; -use puppy::fetch::signatures::Signature; -use serde_json::{from_slice, json, Value}; +use puppy::auth::{self, SigError, Signer, Verifier}; +use puppy::store::Store; +use puppy::{auth::verify_signature, config::Config, Context}; +use serde_json::{from_slice, Value}; use tokio::net::TcpListener; #[tokio::main] @@ -20,13 +19,17 @@ async fn main() { let config = Config { ap_domain: "test.piss-on.me".to_string(), wf_domain: "test.piss-on.me".to_string(), + state_dir: "state".to_string(), port: 1312, }; - start(&config).await.unwrap(); + let store = Store::open(&config.state_dir, puppy::data::schema()).unwrap(); + let verifier = Verifier::load(&config); + let context = Context::new(config, store, verifier); + start(context).await.unwrap(); } -pub async fn start(cfg: &Config) -> Result<(), Box> { - let addr = SocketAddr::from(([127, 0, 0, 1], cfg.port)); +pub async fn start(context: Context) -> Result<(), Box> { + let addr = SocketAddr::from(([127, 0, 0, 1], context.config().port)); let listener = TcpListener::bind(addr).await?; // We start a loop to continuously accept incoming connections @@ -37,13 +40,13 @@ pub async fn start(cfg: &Config) -> Result<(), Box Result<(), Box; type Response> = hyper::Response; -/// The request handler. -async fn handle(req: Request, cfg: Config) -> Result { - // We need to fetch the entire body of the request for signature validation, because that involves making - // a digest of the request body in some cases. - let request = { - let (req, body) = req.into_parts(); - let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else { - return Ok(error::invalid_body("Could not get request body")); - }; - http::Request::from_parts(req, body) - }; - // Simplified representation of a request, so we can pattern match on it more easily in the dispatchers. - let req = make_req(&request); - eprintln!("{request:?}: open"); - // We'll use the path to pick where specifically to send the request. - // Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast - // with a clear error message will save anyone trying to get *their* signatures implementation a major - // headache. - let res = match verify_signature(&request, &cfg).await { - // If the request was signed and the signature was accepted, they can access the protected endpoints. - Ok(Some(sig)) => dispatch_signed(req, sig, cfg).await, - // Unsigned requests can see a smaller subset of endpoints, most notably the verification actor. - Ok(None) => dispatch_public(req, cfg).await, - // If a signature was provided *but it turned out to be unverifiable*, show them the error message. - Err(err) => error::bad_signature(match err { - SigError::VerificationFailed { error } => format!("Verification failed: {error}"), - SigError::ParseSignature { error } => format!("Failed to parse signature: {error}"), - SigError::FailedToFetchKey { keyid } => format!("Failed to fetch {keyid}"), - }), - }; - eprintln!( - "{} {}: done (status: {})", - request.method(), - request.uri(), - res.status() - ); - Ok(res) -} - // A parsed HTTP request for easy handling. struct Req<'a> { method: &'a Method, @@ -109,7 +73,7 @@ impl Req<'_> { } } -fn make_req<'x>(r: &'x http::Request) -> Req<'x> { +fn simplify<'x>(r: &'x http::Request) -> Req<'x> { let path: Vec<&str> = r .uri() .path() @@ -136,37 +100,69 @@ use hyper::Method; const POST: &Method = &Method::POST; const GET: &Method = &Method::GET; +/// The request handler. +async fn handle(req: Request, cx: Context) -> Result { + // We need to fetch the entire body of the request for signature validation, because that involves making + // a digest of the request body in some cases. + let request = { + let (req, body) = req.into_parts(); + let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else { + return Ok(error::invalid_body("Could not get request body")); + }; + http::Request::from_parts(req, body) + }; + // Simplified representation of a request, so we can pattern match on it more easily in the dispatchers. + let req = simplify(&request); + // We'll use the path to pick where specifically to send the request. + // Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast + // with a clear error message will save anyone trying to get *their* signatures implementation a major + // headache. + let res = match verify_signature(&cx, &request).await { + // If the request was signed and the signature was accepted, they can access the protected endpoints. + Ok(Some(sig)) => dispatch_signed(cx, req, sig).await, + // Unsigned requests can see a smaller subset of endpoints, most notably the verification actor. + Ok(None) => dispatch_public(cx, req).await, + // If a signature was provided *but it turned out to be unverifiable*, show them the error message. + Err(err) => error::bad_signature(match err { + SigError::VerificationFailed { error } => format!("Verification failed: {error}"), + SigError::ParseSignature { error } => format!("Failed to parse signature: {error}"), + SigError::FailedToFetchKey { keyid } => format!("Failed to fetch {keyid}"), + }), + }; + Ok(res) +} + /// Handle a signed and verified request. /// /// This function is where all requests to a protected endpoint have to go through. If the request /// was signed but does not target a protected endpoint, this function will fall back to the /// [`dispatch_public`] handler. -async fn dispatch_signed(req: Req<'_>, sig: Signer, cfg: Config) -> Response { +async fn dispatch_signed(cx: Context, req: Req<'_>, sig: Signer) -> Response { eprintln!("Dispatching signed request"); match (req.method, req.path()) { // Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch". // The one exception for this is `/s/request-verifier`, which is where the request // verification actor lives. - (GET, ["o", ulid]) => api::ap::serve_object(ulid), + (GET, ["o", ulid]) => api::ap::serve_object(&cx, ulid), // POSTs to an actor's inbox need to be signed to prevent impersonation. (POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| { // We only handle the intermediate parsing of the json, full resolution of the // activity object will happen inside the inbox handler itself. - api::ap::inbox(ulid, sig, json, cfg) + api::ap::inbox(&cx, ulid, sig, json) }), // Try the resources for which no signature is required as well. - _ => dispatch_public(req, cfg).await, + _ => dispatch_public(cx, req).await, } } /// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the /// function will return a 404 response. -async fn dispatch_public(req: Req<'_>, cfg: Config) -> Response { +async fn dispatch_public(cx: Context, req: Req<'_>) -> Response { eprintln!("Dispatching public request"); match (req.method, req.path()) { - (GET, ["proxy"]) => api::ap::proxy(&req.params).await, - (GET, [".well-known", "webfinger"]) => api::wf::resolve(&req.params, cfg), - (GET, ["s", "request-verifier"]) => api::ap::serve_verifier_actor(cfg), + (GET, ["proxy"]) => api::ap::proxy(&cx, &req.params).await, + (GET, [".well-known", "webfinger"]) => api::wf::resolve(&cx, &req.params), + (GET, auth::VERIFIER_MOUNT) => api::ap::serve_verifier_actor(&cx), (m, p) => { eprintln!("404: {m} {p:?}"); error::not_found() diff --git a/lib/fetch/src/signatures.rs b/lib/fetch/src/signatures.rs index 17d4782..ca8a2e1 100644 --- a/lib/fetch/src/signatures.rs +++ b/lib/fetch/src/signatures.rs @@ -99,22 +99,12 @@ pub struct Private(rsa::RsaPrivateKey); impl Private { /// Generate a new keypair. pub fn gen() -> (Private, Public) { - println!("[!] gen"); let mut rng = rand::thread_rng(); let bits = 512; let private = RsaPrivateKey::new(&mut rng, bits).unwrap(); let public = private.to_public_key(); (Private(private), Public(public)) } - pub fn tee(path: impl AsRef) -> (Private, Public) { - println!("[!] tee"); - let (a, b) = Private::gen(); - println!("[!] keygen complete"); - a.0.write_pkcs8_pem_file(path, LineEnding::default()) - .unwrap(); - println!("[!] write finished"); - (a, b) - } /// Get the public counterpart to this key. pub fn get_public(&self) -> Public { Public(self.0.to_public_key()) @@ -312,7 +302,8 @@ fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result, Strin Ok(buf) } -/// Default for when +/// Maximum time difference between the creation time of the signature and the current time before the +/// signature will be rejected. This is a measure to increase the difficulty of a replay attack. const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5); /// Configuration for the behavior of the signing and verification routines. diff --git a/lib/puppy/src/context.rs b/lib/puppy/src/context.rs index 3fcff8d..0a52594 100644 --- a/lib/puppy/src/context.rs +++ b/lib/puppy/src/context.rs @@ -1,75 +1,55 @@ -use std::sync::OnceLock; - use store::{Key, Store, Transaction}; -use crate::{auth, config::Config, data::schema, Error, Result}; +use crate::{auth::Verifier, config::Config, Result}; /// The context of a running ActivityPuppy. /// /// This type provides access to the data store and configuration. -pub struct Context<'c> { - pub(crate) config: &'c Config, - pub(crate) db: Store, +#[derive(Clone)] +pub struct Context { + verifier: Option, + config: Config, + store: Store, } -impl Context<'_> { +impl Context { + pub fn new(config: Config, store: Store, verifier: Verifier) -> Context { + Context { + verifier: Some(verifier), + config, + store, + } + } /// Do a data store [transaction][store::Transaction]. pub fn run(&self, f: impl FnOnce(&Transaction<'_>) -> Result) -> Result { - self.db.run(f) + self.store.run(f) } /// Access the store directly. pub fn store(&self) -> &Store { - &self.db + &self.store } /// Access the configuration. pub fn config(&self) -> &Config { - self.config + &self.config } /// Create an ActivityPub object ID from a key. pub fn mk_url(&self, key: Key) -> String { format!("https://{}/o/{key}", self.config.ap_domain) } /// Get the verification actor. - pub fn verifier(&self) -> &auth::Verifier { - todo!() + pub fn verifier(&self) -> &Verifier { + self.verifier.as_ref().unwrap() } } -/// The store, which we initialize only once this way. -/// -/// This makes it so we don't have to thread everything down everywhere, we can just access the context -/// when we need it. -static STORE: OnceLock = OnceLock::new(); - -/// Load the puppy [`Context`] from anywhere! -/// -/// This gives you access to the data store and the configuration, without having to thread it through every place. -// WARNING: don't use this within this crate. there's a clippy lint. -pub fn context(f: impl FnOnce(Context<'_>) -> Result) -> Result -where - E: From, -{ - let cfg = Config { - ap_domain: String::from("test.piss-on.me"), - wf_domain: String::from("test.piss-on.me"), - port: 1312, - }; - let db = STORE - .get_or_try_init(|| Store::open(".state", schema())) - .map_err(Error::Store)? - .clone(); - f(Context { db, config: &cfg }) -} - -#[cfg(test)] -use store::types::Schema; - /// Load a context for running tests in. #[cfg(test)] pub fn test_context( config: Config, - schema: Schema, - test: impl FnOnce(Context<'_>) -> Result, + schema: store::types::Schema, + test: impl FnOnce(Context) -> Result, ) -> Result { - Store::test(schema, |db| test(Context { config: &config, db })) + Store::test(schema, |store| { + test(Context { config, store, verifier: None }) + }) } diff --git a/lib/puppy/src/interact.rs b/lib/puppy/src/interact.rs index 576bf56..133bafd 100644 --- a/lib/puppy/src/interact.rs +++ b/lib/puppy/src/interact.rs @@ -1,6 +1,6 @@ //! Interactions between actors. -use store::{util::IterExt as _, Key, StoreError}; +use store::{util::IterExt as _, Key, StoreError, Transaction}; use crate::{ actor::Actor, @@ -29,15 +29,13 @@ impl Actor { /// Makes `biter` bite `victim` and inserts the records into the database. pub fn do_bite(&self, cx: &Context, victim: &Actor) -> Result { let bite = self.bite(victim); - cx.run(|tx| { - tx.create(bite)?; - Ok(bite) - }) + cx.run(|tx| try { tx.create(bite) })?; + Ok(bite) } /// Creates a follow request from `self` to `target`. pub fn do_follow_request(&self, cx: &Context, target: &Actor) -> Result { + let req = self.follow_request(target); cx.run(|tx| { - let req = self.follow_request(target); tx.create(req)?; tx.add_mixin(req.id, Status::Pending)?; Ok(req) @@ -72,49 +70,48 @@ impl Actor { self.key == req.target, "only the target of a follow request may accept it" }; - cx.run(|tx| try { - tx.update(req.id, |_| Status::Rejected)?; - }) + cx.run(|tx| try { tx.update(req.id, |_| Status::Rejected) })?; + Ok(()) } /// Get all pending follow request for `self`. pub fn pending_requests<'c>( &self, - cx: &'c Context, + tx: &'c Transaction<'c>, ) -> impl Iterator> + 'c { - cx.store() - .incoming::(self.key) + tx.incoming::(self.key) .map_err(Error::Store) - .filter_bind_results(|req| Ok(if req.is_pending(cx)? { Some(req) } else { None })) + .filter_bind_results(|req| Ok(if req.is_pending(tx)? { Some(req) } else { None })) } /// Get all nodes `self` is following. - pub fn following<'c>(&self, cx: &'c Context) -> impl Iterator> + 'c { - cx.store() - .outgoing::(self.key) + pub fn following<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator> + 'c { + tx.outgoing::(self.key) .map_err(Error::Store) .map_ok(|a| a.followed) } /// Get all followers of `self`. - pub fn followers<'c>(&self, cx: &'c Context) -> impl Iterator> + 'c { - cx.store() - .incoming::(self.key) + pub fn followers<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator> + 'c { + tx.incoming::(self.key) .map_err(Error::Store) .map_ok(|a| a.follower) } /// List all specific times `self` was bitten. - pub fn bites_suffered<'c>(&self, cx: &'c Context) -> impl Iterator> + 'c { - cx.store().incoming::(self.key).map_err(Error::Store) + pub fn bites_suffered<'c>( + &self, + tx: &'c Transaction<'c>, + ) -> impl Iterator> + 'c { + tx.incoming::(self.key).map_err(Error::Store) } /// Check whether `self` follows `other`. - pub fn follows(&self, cx: &Context, other: &Actor) -> Result { - try { cx.db.exists::(self.key, other.key)? } + pub fn follows(&self, tx: &Transaction<'_>, other: &Actor) -> Result { + try { tx.exists::(self.key, other.key)? } } } impl FollowRequest { /// Determine if this follow request is pending. - pub fn is_pending(&self, cx: &Context) -> Result { + pub fn is_pending(&self, tx: &Transaction<'_>) -> Result { // The status is stored as a mixin, so we need to get it. - let Some(st) = cx.db.get_mixin::(self.id)? else { + let Some(st) = tx.get_mixin::(self.id)? else { // If we don't have a status for a follow request, something is borked. return Err(StoreError::Missing.into()); }; @@ -122,7 +119,7 @@ impl FollowRequest { // relation already exists. debug_assert! { !(st == Status::Pending) - || cx.db.exists::(self.origin, self.target).map(|x| !x)?, + || tx.exists::(self.origin, self.target).map(|x| !x)?, "fr.is_pending -> !(fr.origin follows fr.target)" }; Ok(st == Status::Pending) @@ -150,28 +147,31 @@ mod tests { Config { ap_domain: String::from("unit-test.puppy.gay"), wf_domain: String::from("unit-test.puppy.gay"), + state_dir: todo!(), // TODO: make this a temp dir + port: 0, } } #[test] fn create_fr() -> Result<()> { - test_context(test_config(), schema(), |cx| try { - let db = cx.store(); + test_context(test_config(), schema(), |cx| { let (alice, bob) = make_test_actors(&cx)?; alice.do_follow_request(&cx, &bob)?; assert!( - db.exists::(alice.key, bob.key)?, + cx.store().exists::(alice.key, bob.key)?, "(alice -> bob) ∈ follow-requested" ); assert!( - !db.exists::(alice.key, bob.key)?, + !cx.store().exists::(alice.key, bob.key)?, "(alice -> bob) ∉ follows" ); - let pending_for_bob = bob - .pending_requests(&cx) - .map_ok(|fr| fr.origin) - .try_collect::>()?; + let pending_for_bob = cx.run(|tx| { + bob.pending_requests(&tx) + .map_ok(|fr| fr.origin) + .try_collect::>() + })?; assert_eq!(pending_for_bob, vec![alice.key], "bob.pending = {{alice}}"); + Ok(()) }) } @@ -192,16 +192,17 @@ mod tests { "(bob -> alice) ∉ follows" ); - let pending_for_bob: Vec<_> = bob.pending_requests(&cx).try_collect()?; - assert!(pending_for_bob.is_empty(), "bob.pending = ∅"); + cx.run(|tx| try { + let pending_for_bob: Vec<_> = bob.pending_requests(&tx).try_collect()?; + assert!(pending_for_bob.is_empty(), "bob.pending = ∅"); - let followers_of_bob: Vec<_> = bob.followers(&cx).try_collect()?; - assert_eq!( - followers_of_bob, - vec![alice.key], - "bob.followers = {{alice}}" - ); - Ok(()) + let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?; + assert_eq!( + followers_of_bob, + vec![alice.key], + "bob.followers = {{alice}}" + ); + }) }) } @@ -212,19 +213,21 @@ mod tests { let req = alice.do_follow_request(&cx, &bob)?; bob.do_accept_request(&cx, req)?; - let followers_of_bob: Vec<_> = bob.followers(&cx).try_collect()?; - assert_eq!( - followers_of_bob, - vec![alice.key], - "bob.followers = {{alice}}" - ); + cx.run(|tx| try { + let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?; + assert_eq!( + followers_of_bob, + vec![alice.key], + "bob.followers = {{alice}}" + ); - let following_of_alice: Vec<_> = alice.following(&cx).try_collect()?; - assert_eq!( - following_of_alice, - vec![bob.key], - "alice.following = {{bob}}" - ); + let following_of_alice: Vec<_> = alice.following(&tx).try_collect()?; + assert_eq!( + following_of_alice, + vec![bob.key], + "alice.following = {{bob}}" + ); + })? }) } } diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index 5d7f15b..f615348 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -12,12 +12,13 @@ // but that would make every type signature ever 100x more complicated, so we're not doing it. #![deny(clippy::disallowed_methods, clippy::disallowed_types)] -pub use context::{context, Context}; +pub use context::Context; #[cfg(test)] pub use context::test_context; use data::{ActivityKind, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey}; +use store::Transaction; pub use store::{self, Key, StoreError}; pub use fetch; @@ -30,21 +31,21 @@ mod interact; /// Retrieve an ActivityPub object from the database. /// /// Fails with `Error::Missing` if the required properties are not present. -pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result { - let Some(obj) = cx.db.get_mixin::(key)? else { +pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result { + let Some(obj) = tx.get_mixin::(key)? else { // We need this data in order to determine the object type. If the passed key does not // have this data, it must not be an ActivityPub object. return Err(Error::MissingData { node: key, prop: "Object" }); }; match obj.kind { ObjectKind::Actor => { - let Some(Profile { account_name, display_name, .. }) = cx.db.get_mixin(key)? else { + let Some(Profile { account_name, display_name, .. }) = tx.get_mixin(key)? else { return Err(Error::MissingData { node: key, prop: "Profile" }); }; - let Some(Channel { inbox }) = cx.db.get_mixin(key)? else { + let Some(Channel { inbox }) = tx.get_mixin(key)? else { return Err(Error::MissingData { node: key, prop: "Channel" }); }; - let Some(PublicKey { key_id, key_pem }) = cx.db.get_mixin(key)? else { + let Some(PublicKey { key_id, key_pem }) = tx.get_mixin(key)? else { return Err(Error::MissingData { node: key, prop: "PublicKey" }); }; Ok(fetch::Object::Actor(fetch::Actor { @@ -60,19 +61,19 @@ pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result })) } ObjectKind::Activity(ActivityKind::Create) => { - let Some(Create { object, actor, .. }) = cx.db.get_arrow(key)? else { + let Some(Create { object, actor, .. }) = tx.get_arrow(key)? else { panic!("expected a `Create`"); }; - let Id(actor) = cx.db.get_alias(actor)?.unwrap(); + let Id(actor) = tx.get_alias(actor)?.unwrap(); Ok(fetch::Object::Activity(fetch::Activity { id: obj.id.0.into(), actor: actor.into(), - object: Box::new(get_local_ap_object(cx, object)?), + object: Box::new(get_local_ap_object(tx, object)?), kind: String::from("Create"), })) } ObjectKind::Notelike(kind) => { - let Some(Content { content, warning, .. }) = cx.db.get_mixin(key)? else { + let Some(Content { content, warning, .. }) = tx.get_mixin(key)? else { panic!() }; Ok(fetch::Object::Object { @@ -105,9 +106,8 @@ pub mod actor { impl Actor { /// Get a local actor from the store by their username. - pub fn by_username(cx: &Context, username: impl ToString) -> Result> { - let maybe_key = cx - .store() + pub fn by_username(tx: &Transaction<'_>, username: impl ToString) -> Result> { + let maybe_key = tx .lookup(Username(username.to_string())) .map_err(Error::Store)?; // For now, we only have local actors. @@ -125,8 +125,8 @@ pub mod actor { cx.run(|tx| { let username: Username = username.to_string().into(); // Federation stuff - mixin_ap_actor(tx, key, &cx.config.ap_domain, true)?; - mixin_priv_key(tx, key, &cx.config.ap_domain)?; + mixin_ap_actor(tx, key, &cx.config().ap_domain, true)?; + mixin_priv_key(tx, key, &cx.config().ap_domain)?; // Social properties tx.add_alias(key, username.clone())?; tx.add_mixin(key, Profile { @@ -195,6 +195,7 @@ pub mod config { pub struct Config { pub ap_domain: String, pub wf_domain: String, + pub state_dir: String, pub port: u16, } } @@ -203,8 +204,10 @@ pub mod auth { use fetch::signatures::{Private, Public, Signature}; use serde_json::{json, Value}; - use crate::config::Config; + use crate::{config::Config, Context}; + /// Checks request signatures. + #[derive(Clone)] pub struct Verifier { actor_id: String, key_id: String, @@ -212,13 +215,20 @@ pub mod auth { public: Public, } + const VERIFIER_PATH: &str = "/s/request-verifier"; + /// The path at which the request verification actor will present itself. + pub const VERIFIER_MOUNT: &[&str] = &["s", "request-verifier"]; + impl Verifier { - pub async fn get_public_key(&self, uri: &str) -> Result { + /// Send a request to get the public key from an ID. This request will be signed with the + /// verifier actor's public key. + async fn fetch_public_key(&self, uri: &str) -> Result, String> { let json = fetch::resolve(&self.signing_key(), uri).await.unwrap(); - Ok(fetch::Key::from_json(dbg!(json)).unwrap()) + Ok(fetch::Key::from_json(dbg!(json)).unwrap().upgrade()) } - pub fn signing_key(&self) -> fetch::SigningKey { + /// Get the key that the verification actor signs requests with. + fn signing_key(&self) -> fetch::SigningKey { fetch::Key { id: self.key_id.clone(), owner: self.actor_id.clone(), @@ -226,6 +236,7 @@ pub mod auth { } } + /// Get the JSON-LD representation of the verifier actor. pub fn to_json_ld(&self) -> Value { json!({ "@context": [ @@ -243,14 +254,15 @@ pub mod auth { }) } + /// Load the actor's verifier actor. pub fn load(cfg: &Config) -> Verifier { println!("[*] loading private key"); - let domain = &cfg.ap_domain; - let private = Private::load(".state/fetcher.pem"); + let Config { ap_domain, state_dir, .. } = cfg; + let private = Private::load(format!("{state_dir}/fetcher.pem")); println!("* done loading private key"); Verifier { - actor_id: format!("https://{domain}/s/request-verifier"), - key_id: format!("https://{domain}/s/request-verifier#sig-key"), + actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"), + key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"), public: private.get_public(), private, } @@ -269,21 +281,22 @@ pub mod auth { VerificationFailed { error: String }, } + // TODO: make it so we don't have to know what an "http request" is in this crate. /// Check the signature for a request. pub async fn verify_signature( + cx: &Context, req: &fetch::http::Request + std::fmt::Debug>, - cfg: &Config, ) -> Result, SigError> { println!(">>> starting signature verification for {req:#?}"); - if req.uri().path() == "/s/request-verifier" { - // Allow access to the request verifier actor without checking the signature. + if req.uri().path() == VERIFIER_PATH { + // HACK: Allow access to the request verifier actor without checking the signature. return Ok(None); } println!(">>> not going for the verifier!"); if req.headers().get("signature").is_none() { - // Request is not signed! + // Request is not signed. return Ok(None); }; println!(">>> has signature"); @@ -299,8 +312,8 @@ pub mod auth { println!(">>> signature is syntatically valid"); // Fetch the public key using the verifier private key. - let verifier = Verifier::load(cfg); - let Ok(public_key) = verifier.get_public_key(sig.key_id()).await else { + let verifier = cx.verifier(); + let Ok(public_key) = verifier.fetch_public_key(sig.key_id()).await else { return Err(SigError::FailedToFetchKey { keyid: sig.key_id().to_string(), }); @@ -308,14 +321,12 @@ pub mod auth { println!(">>> public key fetched"); // Verify the signature header on the request. - let public_key = public_key.upgrade(); - println!(">>> upgraded"); if let Err(error) = public_key.verify(&sig) { println!(">>> rejected"); Err(SigError::VerificationFailed { error }) } else { println!(">>> request verified"); - Ok(Some(Signer { ap_id: public_key.owner.into() })) + Ok(Some(Signer { ap_id: public_key.owner })) } } } diff --git a/lib/puppy/src/post.rs b/lib/puppy/src/post.rs index b932d5e..006a827 100644 --- a/lib/puppy/src/post.rs +++ b/lib/puppy/src/post.rs @@ -17,53 +17,6 @@ pub struct Post { pub key: Key, } -impl Post { - pub async fn federate(&self, cx: &Context<'_>) -> crate::Result<()> { - use fetch::{Activity, Object}; - let post @ Object::Object { .. } = crate::get_local_ap_object(cx, self.key)? else { - todo!() - }; - let author = cx - .db - .incoming::(self.key) - .map_ok(|a| a.author) - .next() - .unwrap()?; - let author_id = cx.db.get_alias::(author)?.unwrap(); - let activity = Activity { - id: post.id().clone().into(), - object: Box::new(post), - kind: String::from("Create"), - actor: author_id.0.clone().into(), - }; - let (private, public) = cx.db.get_mixin_many::<(PrivateKey, PublicKey)>(author)?; - let key = fetch::SigningKey { - id: public.key_id, - owner: author_id.0, - inner: Private::decode_pem(&private.key_pem), - }; - // Insert the activity in the database. - cx.run(|tx| try { - let activity_key = Key::gen(); - let id: data::Id = cx.mk_url(activity_key).into(); - tx.add_mixin(activity_key, data::Object { - id: id.clone(), - kind: ObjectKind::Activity(data::ActivityKind::Create), - local: true, - })?; - tx.create(data::Create { - id: activity_key, - actor: author, - object: self.key, - })?; - tx.add_alias(activity_key, id)?; - })?; - // Send the requests. - fetch::deliver(&key, activity, "https://crimew.gay/users/ezri/inbox").await; - Ok(()) - } -} - impl From<&str> for Content { fn from(value: &str) -> Self { value.to_string().into() @@ -180,7 +133,7 @@ pub fn create_post(cx: &Context, author: Key, content: impl Into) -> cr // Local stuff mixin_post(tx, key, author, content)?; // Federation stuff - let id = Id(format!("https://{}/o/{key}", cx.config.ap_domain)); + let id = Id(cx.mk_url(key)); tx.add_alias(key, id.clone())?; tx.add_mixin(key, Object { kind: ObjectKind::Notelike("Note".to_string()), diff --git a/lib/store/src/arrow.rs b/lib/store/src/arrow.rs index d067472..96bf90e 100644 --- a/lib/store/src/arrow.rs +++ b/lib/store/src/arrow.rs @@ -217,6 +217,18 @@ impl Transaction<'_> { { op::between::(self, a, b).map_ok(A::from) } + /// Construct the arrow from its identifier. + pub fn get_arrow(&self, key: Key) -> Result> + where + A: Arrow, + { + let arrow = self + .open(crate::types::MULTIEDGE_HEADERS) + .get(key)? + .map(|v| Key::split(v.as_ref())) + .map(|(origin, target)| A::from(Multi { origin, target, identity: key })); + Ok(arrow) + } } impl Batch { diff --git a/lib/store/src/mixin.rs b/lib/store/src/mixin.rs index 98c1f46..542e94d 100644 --- a/lib/store/src/mixin.rs +++ b/lib/store/src/mixin.rs @@ -128,6 +128,13 @@ impl Transaction<'_> { { op::join_on(self, iter.into_iter().map_ok(f)) } + /// Get multiple mixins associated with the same key. + pub fn get_mixin_many(&self, key: Key) -> Result + where + T: GetMany, + { + T::get(self, key) + } } impl Batch {