Improve error handling in server::api
This commit is contained in:
parent
564771931f
commit
edc21b4403
6 changed files with 166 additions and 188 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1547,6 +1547,7 @@ dependencies = [
|
|||
name = "server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
|
|
|
@ -10,3 +10,4 @@ http-body-util = "*"
|
|||
hyper-util = { version = "*", features = ["full"] }
|
||||
serde_json = "*"
|
||||
http = "*"
|
||||
derive_more = "*"
|
||||
|
|
|
@ -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
66
bin/server/src/api/ap.rs
Normal 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
70
bin/server/src/api/wf.rs
Normal 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
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
|
@ -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>;
|
||||
|
|
Loading…
Reference in a new issue