From edc21b440310edfb2c83516f9d149e6302bf5d9c Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Thu, 2 May 2024 21:23:45 +0200 Subject: [PATCH] Improve error handling in server::api --- Cargo.lock | 1 + bin/server/Cargo.toml | 1 + bin/server/src/api.rs | 206 ++++----------------------------------- bin/server/src/api/ap.rs | 66 +++++++++++++ bin/server/src/api/wf.rs | 70 +++++++++++++ lib/puppy/src/lib.rs | 10 +- 6 files changed, 166 insertions(+), 188 deletions(-) create mode 100644 bin/server/src/api/ap.rs create mode 100644 bin/server/src/api/wf.rs diff --git a/Cargo.lock b/Cargo.lock index db91675..802d527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1547,6 +1547,7 @@ dependencies = [ name = "server" version = "0.0.0" dependencies = [ + "derive_more", "http", "http-body-util", "hyper", diff --git a/bin/server/Cargo.toml b/bin/server/Cargo.toml index 04d167f..14a8034 100644 --- a/bin/server/Cargo.toml +++ b/bin/server/Cargo.toml @@ -10,3 +10,4 @@ http-body-util = "*" hyper-util = { version = "*", features = ["full"] } serde_json = "*" http = "*" +derive_more = "*" diff --git a/bin/server/src/api.rs b/bin/server/src/api.rs index 4c88321..be311d8 100644 --- a/bin/server/src/api.rs +++ b/bin/server/src/api.rs @@ -18,164 +18,20 @@ use crate::sig::{Signer, Verdict, Verifier, VERIFIER_MOUNT}; use self::error::Message; -pub mod ap { - //! ActivityPub handlers. - - use http_body_util::Full; - use hyper::body::Bytes; - use puppy::{ - actor::Actor, - data::{Id, PrivateKey, PublicKey}, - fetch::signatures::{Private, SigningKey}, - get_local_ap_object, Context, Key, - }; - use serde_json::{to_string, Value}; - - use crate::sig::{Signer, Verifier}; - use super::{ - error::{self, Message}, - respond, Response, - }; - - /// Proxy a request through the instance. - 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"), []); - }; - let Some(url) = params.iter().find_map(|(k, v)| (*k == "url").then_some(v)) else { - return respond(400, Some("Expected `url` query param"), []); - }; - - // Look up the actor's key in the store (which is accessible through the puppy context). - let Ok(signing_key) = cx.run(|tx| { - let actor = Actor::by_username(&tx, user)?.unwrap(); - let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) = - tx.get_mixin_many(actor.key)?; - let Id(owner) = tx.get_alias(actor.key)?.unwrap(); - let inner = Private::decode_pem(&key_pem); - Ok(SigningKey { id: key_id, owner, inner }) - }) else { - panic!("failed to get signing key"); - }; - - eprintln!("proxy: params: {params:?}"); - // Proxy the request through our fetcher. - let resp = puppy::fetch::forward(&signing_key, url).await.unwrap(); - eprintln!("proxy: status = {}", resp.status()); - - // Convert the http-types request to a hyper request. - resp.map(Bytes::from).map(Full::new).into() - } - - /// Handle POSTs to actor inboxes. Requires request signature. - 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(cx: &Context, object_ulid: &str) -> Result { - let Ok(parsed) = object_ulid.parse::() else { - return Err(Message { - error: "improperly formatted ulid", - ..error::BAD_REQUEST - }); - }; - let result = cx.run(|tx| get_local_ap_object(&tx, parsed)); - let Ok(object) = result else { - return Err(error::NOT_FOUND); - }; - let json = to_string(&object.to_json_ld()).unwrap(); - Ok(respond(200, Some(json), [AP_CONTENT_TYPE])) - } - - const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json"); - - /// Serve the special actor used for signing requests. - pub fn serve_verifier_actor(verifier: &Verifier) -> Response { - let body = verifier.to_json_ld(); - let encoded = serde_json::to_vec(&body).unwrap(); - respond(200, Some(encoded), [AP_CONTENT_TYPE]) - } -} - -pub mod wf { - //! WebFinger endpoints and related stuff. - - use puppy::{ - data::{Id, Username}, - Context, - }; - use serde_json::{json, Value}; - - use super::{ - error::{Message, BAD_REQUEST, INTERNAL, NOT_FOUND}, - respond, Response, - }; - - const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json"); - - /// Respond to a webfinger request. - pub fn resolve(cx: &Context, params: &[(&str, &str)]) -> Result { - match params.iter().find_map(get_handle) { - Some(handle) if cx.config().wf_domain == handle.instance => { - let username = Username(handle.username.to_string()); - let Ok(Some(user)) = cx.store().lookup(username) else { - do yeet NOT_FOUND; - }; - let Ok(Some(Id(id))) = cx.store().get_alias(user) else { - do yeet INTERNAL; - }; - let jrd = make_jrd(handle, &id); - let encoded = serde_json::to_vec(&jrd).unwrap(); - Ok(respond(200, Some(encoded), [WF_CONTENT_TYPE])) - } - Some(_) | None => Err(Message { - error: "missing/invalid resource parameter", - ..BAD_REQUEST - }), - } - } - - #[derive(Clone, Copy)] - pub struct Handle<'x> { - username: &'x str, - instance: &'x str, - } - - /// Parse the `resource` parameter into a [`Handle`]. - fn get_handle<'x>((k, v): &'x (&str, &str)) -> Option> { - // We're looking for the `resource` query parameter. - if *k == "resource" { - // This prefix needs to exist according to spec. - let (username, instance) = v - .strip_prefix("acct:")? - // Some implementations may prefix with `@`. its ok if it's there and its also ok - // if its not there, so we use `trim_start_matches` instead of `strip_prefix`. - .trim_start_matches('@') - // Split on the middle `@` symbol, which separates the username and instance bits - .split_once('@')?; - Some(Handle { username, instance }) - } else { - None - } - } - - /// Construct a "JSON resource descriptor". - fn make_jrd(handle: Handle<'_>, id: &str) -> Value { - json!({ - "subject": format!("acct:{}@{}", handle.username, handle.instance), - "links": [ - { - "rel": "self", - "type": "application/activity+json", - "href": id - }, - ] +// A simple macro for returning an error message. +macro_rules! fuck { + ($code:literal: $($arg:tt)*) => { + return Err(crate::api::error::Message { + status: $code, + error: format!($($arg)*), + detail: None, }) - } + }; } +pub mod ap; +pub mod wf; + type Request = hyper::Request; type Response> = hyper::Response; @@ -279,7 +135,7 @@ async fn handle(req: Request, verifier: &Verifier, cx: Context) -> Result dispatch_public(cx, &verifier, &req).await, // If a signature was provided *but it turned out to be unverifiable*, show them the error message. Verdict::Rejected { reason, signature_str } => Err(Message { - error: "signature verification failed", + error: String::from("signature verification failed for request"), status: 403, detail: Some(json!({ "signature": signature_str, @@ -331,11 +187,11 @@ async fn dispatch_public( req: &Req<'_>, ) -> Result { match (req.method, req.path()) { - (GET, ["proxy"]) => Ok(ap::proxy(&cx, &req.params).await), + (GET, ["proxy"]) => ap::proxy(&cx, &req.params).await, (GET, [".well-known", "webfinger"]) => wf::resolve(&cx, &req.params), // TODO: nicer solution for this (GET, VERIFIER_MOUNT) => Ok(ap::serve_verifier_actor(&verifier)), - _ => Err(error::NOT_FOUND), + _ => fuck!(404: "not found"), } } @@ -343,13 +199,10 @@ fn with_json( body: &[u8], f: impl FnOnce(Value) -> Result, ) -> Result { - let Ok(json) = from_slice(body) else { - return Err(Message { - error: "could not decode json", - ..error::BAD_REQUEST - }); - }; - f(json) + match from_slice(body) { + Ok(json) => f(json), + Err(e) => fuck!(400: "could not decode json: {e}"), + } } /// A quick, simple way to construct a response. @@ -378,7 +231,7 @@ mod error { /// An error message shown to an end user of the API. pub struct Message { /// The main error message. - pub error: &'static str, + pub error: String, /// Only shown if the `accept` header included json. pub detail: Option, /// The status code for the response. @@ -414,25 +267,4 @@ mod error { .any(|(k, v)| k == "application" && v.split('+').any(|p| p == "json")) } } - - /// A 404 NOT FOUND response. - pub const NOT_FOUND: Message = Message { - error: "not found", - detail: None, - status: 404, - }; - - /// A basic 400 BAD REQUEST response. - pub const BAD_REQUEST: Message = Message { - error: "bad request", - detail: None, - status: 400, - }; - - /// A basic 500 INTERNAL SERVER ERROR message. - pub const INTERNAL: Message = Message { - error: "internal server error", - detail: None, - status: 500, - }; } diff --git a/bin/server/src/api/ap.rs b/bin/server/src/api/ap.rs new file mode 100644 index 0000000..cf00d6a --- /dev/null +++ b/bin/server/src/api/ap.rs @@ -0,0 +1,66 @@ +//! ActivityPub handlers. + +use http_body_util::Full; +use hyper::body::Bytes; +use puppy::{ + actor::{get_signing_key, Actor}, + get_local_ap_object, Context, Error, Key, +}; +use serde_json::{to_string, Value}; + +use crate::sig::{Signer, Verifier}; +use super::{error::Message, respond, Response}; + +/// Proxy a request through the instance. +pub async fn proxy(cx: &Context, params: &[(&str, &str)]) -> Result { + // Extract our query parameters. + let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else { + fuck!(400: "Expected `user` query param"); + }; + let Some(url) = params.iter().find_map(|(k, v)| (*k == "url").then_some(v)) else { + fuck!(400: "Expected `url` query param"); + }; + + // Look up the actor's key in the store (which is accessible through the puppy context). + let Ok(signing_key) = cx.run(|tx| { + let actor = Actor::by_username(&tx, user)?.unwrap(); + get_signing_key(tx, actor).map_err(Error::from) + }) else { + fuck!(500: "failed to get signing key"); + }; + + eprintln!("proxy: params: {params:?}"); + // Proxy the request through our fetcher. + let resp = puppy::fetch::forward(&signing_key, url).await.unwrap(); + eprintln!("proxy: status = {}", resp.status()); + + // Convert the http-types request to a hyper request. + Ok(resp.map(Bytes::from).map(Full::new).into()) +} + +/// Handle POSTs to actor inboxes. Requires request signature. +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(cx: &Context, object_ulid: &str) -> Result { + let Ok(parsed) = object_ulid.parse::() else { + fuck!(400: "improperly formatted ulid"); + }; + let result = cx.run(|tx| get_local_ap_object(&tx, parsed)); + let Ok(object) = result else { + fuck!(404: "object does not exist"); + }; + let json = to_string(&object.to_json_ld()).unwrap(); + Ok(respond(200, Some(json), [AP_CONTENT_TYPE])) +} + +const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json"); + +/// Serve the special actor used for signing requests. +pub fn serve_verifier_actor(verifier: &Verifier) -> Response { + let body = verifier.to_json_ld(); + let encoded = serde_json::to_vec(&body).unwrap(); + respond(200, Some(encoded), [AP_CONTENT_TYPE]) +} diff --git a/bin/server/src/api/wf.rs b/bin/server/src/api/wf.rs new file mode 100644 index 0000000..3076399 --- /dev/null +++ b/bin/server/src/api/wf.rs @@ -0,0 +1,70 @@ +//! WebFinger endpoints and related stuff. + +use puppy::{ + data::{Id, Username}, + Context, +}; +use serde_json::{json, Value}; +use derive_more::Display; + +use super::{error::Message, respond, Response}; + +const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json"); + +/// Respond to a webfinger request. +pub fn resolve(cx: &Context, params: &[(&str, &str)]) -> Result { + match params.iter().find_map(get_handle) { + Some(handle) if cx.config().wf_domain == handle.instance => { + let username = Username(handle.username.to_string()); + let Ok(Some(user)) = cx.store().lookup(username) else { + fuck!(404: "no user {}@{} exists", handle.username, handle.instance); + }; + let Ok(Some(Id(id))) = cx.store().get_alias(user) else { + fuck!(500: "internal error"); + }; + let jrd = make_jrd(handle, &id); + let encoded = serde_json::to_vec(&jrd).unwrap(); + Ok(respond(200, Some(encoded), [WF_CONTENT_TYPE])) + } + Some(_) | None => fuck!(400: "missing/invalid resource param"), + } +} + +#[derive(Clone, Copy, Display)] +#[display(fmt = "@{username}@{instance}")] +pub struct Handle<'x> { + username: &'x str, + instance: &'x str, +} + +/// Parse the `resource` parameter into a [`Handle`]. +fn get_handle<'x>((k, v): &'x (&str, &str)) -> Option> { + // We're looking for the `resource` query parameter. + if *k == "resource" { + // This prefix needs to exist according to spec. + let (username, instance) = v + .strip_prefix("acct:")? + // Some implementations may prefix with `@`. its ok if it's there and its also ok + // if its not there, so we use `trim_start_matches` instead of `strip_prefix`. + .trim_start_matches('@') + // Split on the middle `@` symbol, which separates the username and instance bits + .split_once('@')?; + Some(Handle { username, instance }) + } else { + None + } +} + +/// Construct a "JSON resource descriptor". +fn make_jrd(handle: Handle<'_>, id: &str) -> Value { + json!({ + "subject": format!("acct:{}@{}", handle.username, handle.instance), + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": id + }, + ] + }) +} diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index eabcf56..39102bd 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -88,7 +88,7 @@ pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result, actor: Actor) -> Result { + let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) = + tx.get_mixin_many(actor.key)?; + let Id(owner) = tx.get_alias(actor.key)?.unwrap(); + let inner = Private::decode_pem(&key_pem); + Ok(SigningKey { id: key_id, owner, inner }) + } } pub type Result = std::result::Result;