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"
|
name = "server"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"derive_more",
|
||||||
"http",
|
"http",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
|
|
@ -10,3 +10,4 @@ http-body-util = "*"
|
||||||
hyper-util = { version = "*", features = ["full"] }
|
hyper-util = { version = "*", features = ["full"] }
|
||||||
serde_json = "*"
|
serde_json = "*"
|
||||||
http = "*"
|
http = "*"
|
||||||
|
derive_more = "*"
|
||||||
|
|
|
@ -18,164 +18,20 @@ use crate::sig::{Signer, Verdict, Verifier, VERIFIER_MOUNT};
|
||||||
|
|
||||||
use self::error::Message;
|
use self::error::Message;
|
||||||
|
|
||||||
pub mod ap {
|
// A simple macro for returning an error message.
|
||||||
//! ActivityPub handlers.
|
macro_rules! fuck {
|
||||||
|
($code:literal: $($arg:tt)*) => {
|
||||||
use http_body_util::Full;
|
return Err(crate::api::error::Message {
|
||||||
use hyper::body::Bytes;
|
status: $code,
|
||||||
use puppy::{
|
error: format!($($arg)*),
|
||||||
actor::Actor,
|
detail: None,
|
||||||
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
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod ap;
|
||||||
|
pub mod wf;
|
||||||
|
|
||||||
type Request = hyper::Request<hyper::body::Incoming>;
|
type Request = hyper::Request<hyper::body::Incoming>;
|
||||||
type Response<T = Full<Bytes>> = hyper::Response<T>;
|
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,
|
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.
|
// If a signature was provided *but it turned out to be unverifiable*, show them the error message.
|
||||||
Verdict::Rejected { reason, signature_str } => Err(Message {
|
Verdict::Rejected { reason, signature_str } => Err(Message {
|
||||||
error: "signature verification failed",
|
error: String::from("signature verification failed for request"),
|
||||||
status: 403,
|
status: 403,
|
||||||
detail: Some(json!({
|
detail: Some(json!({
|
||||||
"signature": signature_str,
|
"signature": signature_str,
|
||||||
|
@ -331,11 +187,11 @@ async fn dispatch_public(
|
||||||
req: &Req<'_>,
|
req: &Req<'_>,
|
||||||
) -> Result<Response, Message> {
|
) -> Result<Response, Message> {
|
||||||
match (req.method, req.path()) {
|
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),
|
(GET, [".well-known", "webfinger"]) => wf::resolve(&cx, &req.params),
|
||||||
// TODO: nicer solution for this
|
// TODO: nicer solution for this
|
||||||
(GET, VERIFIER_MOUNT) => Ok(ap::serve_verifier_actor(&verifier)),
|
(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],
|
body: &[u8],
|
||||||
f: impl FnOnce(Value) -> Result<Response, Message>,
|
f: impl FnOnce(Value) -> Result<Response, Message>,
|
||||||
) -> Result<Response, Message> {
|
) -> Result<Response, Message> {
|
||||||
let Ok(json) = from_slice(body) else {
|
match from_slice(body) {
|
||||||
return Err(Message {
|
Ok(json) => f(json),
|
||||||
error: "could not decode json",
|
Err(e) => fuck!(400: "could not decode json: {e}"),
|
||||||
..error::BAD_REQUEST
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
f(json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A quick, simple way to construct a response.
|
/// 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.
|
/// An error message shown to an end user of the API.
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
/// The main error message.
|
/// The main error message.
|
||||||
pub error: &'static str,
|
pub error: String,
|
||||||
/// Only shown if the `accept` header included json.
|
/// Only shown if the `accept` header included json.
|
||||||
pub detail: Option<Value>,
|
pub detail: Option<Value>,
|
||||||
/// The status code for the response.
|
/// The status code for the response.
|
||||||
|
@ -414,25 +267,4 @@ mod error {
|
||||||
.any(|(k, v)| k == "application" && v.split('+').any(|p| p == "json"))
|
.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 {
|
pub mod actor {
|
||||||
use fetch::signatures::Private;
|
use fetch::signatures::{Private, SigningKey};
|
||||||
use store::{Key, StoreError, Transaction};
|
use store::{Key, StoreError, Transaction};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -173,6 +173,14 @@ pub mod actor {
|
||||||
tx.add_mixin(vertex, PrivateKey { key_pem: private.encode_pem() })?;
|
tx.add_mixin(vertex, PrivateKey { key_pem: private.encode_pem() })?;
|
||||||
store::OK
|
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>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
Loading…
Reference in a new issue