Improve error handling in server::api

This commit is contained in:
Riley Apeldoorn 2024-05-02 21:23:45 +02:00
parent 564771931f
commit edc21b4403
6 changed files with 166 additions and 188 deletions

1
Cargo.lock generated
View file

@ -1547,6 +1547,7 @@ dependencies = [
name = "server"
version = "0.0.0"
dependencies = [
"derive_more",
"http",
"http-body-util",
"hyper",

View file

@ -10,3 +10,4 @@ http-body-util = "*"
hyper-util = { version = "*", features = ["full"] }
serde_json = "*"
http = "*"
derive_more = "*"

View file

@ -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<Response, Message> {
let Ok(parsed) = object_ulid.parse::<Key>() 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<Response, Message> {
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<Handle<'x>> {
// 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<hyper::body::Incoming>;
type Response<T = Full<Bytes>> = hyper::Response<T>;
@ -279,7 +135,7 @@ async fn handle(req: Request, verifier: &Verifier, cx: Context) -> Result<Respon
Verdict::Unsigned => 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<Response, Message> {
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<Response, Message>,
) -> Result<Response, Message> {
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<Value>,
/// 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,
};
}

66
bin/server/src/api/ap.rs Normal file
View file

@ -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<Response, Message> {
// 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<Response, Message> {
let Ok(parsed) = object_ulid.parse::<Key>() 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])
}

70
bin/server/src/api/wf.rs Normal file
View file

@ -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<Response, Message> {
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<Handle<'x>> {
// 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
},
]
})
}

View file

@ -88,7 +88,7 @@ pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::obje
}
pub mod actor {
use fetch::signatures::Private;
use fetch::signatures::{Private, SigningKey};
use store::{Key, StoreError, Transaction};
use crate::{
@ -173,6 +173,14 @@ pub mod actor {
tx.add_mixin(vertex, PrivateKey { key_pem: private.encode_pem() })?;
store::OK
}
pub fn get_signing_key(tx: &Transaction<'_>, actor: Actor) -> Result<SigningKey, StoreError> {
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<T, E = Error> = std::result::Result<T, E>;