A whole bunch of different refactors
This commit is contained in:
parent
09cf289b75
commit
8d350e8cd9
10 changed files with 273 additions and 300 deletions
|
@ -1,59 +1,73 @@
|
||||||
#![feature(iterator_try_collect)]
|
#![feature(iterator_try_collect)]
|
||||||
use puppy::{
|
use puppy::{
|
||||||
actor::Actor,
|
actor::Actor,
|
||||||
data::{Bite, Profile},
|
auth::Verifier,
|
||||||
|
config::Config,
|
||||||
|
data::{schema, Bite, Profile},
|
||||||
post::Author,
|
post::Author,
|
||||||
store::util::IterExt as _,
|
store::{util::IterExt as _, Store},
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> puppy::Result<()> {
|
fn main() -> puppy::Result<()> {
|
||||||
// puppy::store::Store::nuke(".state")?;
|
// puppy::store::Store::nuke(".state")?;
|
||||||
puppy::context(|cx| {
|
let config = Config {
|
||||||
let db = cx.store();
|
ap_domain: "test.piss-on.me".to_string(),
|
||||||
println!("creating actors");
|
wf_domain: "test.piss-on.me".to_string(),
|
||||||
let riley = get_or_create_actor(&cx, "riley")?;
|
state_dir: ".state".to_string(),
|
||||||
let linen = get_or_create_actor(&cx, "linen")?;
|
port: 1312,
|
||||||
if true {
|
};
|
||||||
println!("creating posts");
|
let verifier = Verifier::load(&config);
|
||||||
puppy::post::create_post(&cx, riley.key, "@linen <3")?;
|
let db = Store::open(&config.state_dir, schema())?;
|
||||||
puppy::post::create_post(&cx, linen.key, "@riley <3")?;
|
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 {
|
if true {
|
||||||
println!("making riley follow linen");
|
println!("making riley follow linen");
|
||||||
if !riley.follows(&cx, &linen)? {
|
|
||||||
|
cx.run(|tx| {
|
||||||
|
if !riley.follows(&tx, &linen)? {
|
||||||
println!("follow relation does not exist yet");
|
println!("follow relation does not exist yet");
|
||||||
if let Some(req) = linen
|
if let Some(req) = linen
|
||||||
.pending_requests(&cx)
|
.pending_requests(&tx)
|
||||||
.find_ok(|r| r.origin == riley.key)?
|
.find_ok(|r| r.origin == riley.key)?
|
||||||
{
|
{
|
||||||
println!("accepting the pending follow request");
|
println!("accepting the pending follow request");
|
||||||
linen.do_accept_request(&cx, req)?;
|
linen.do_accept_request(&cx, req)
|
||||||
} else {
|
} else {
|
||||||
println!("no pending follow request; creating");
|
println!("no pending follow request; creating");
|
||||||
riley.do_follow_request(&cx, &linen)?;
|
riley.do_follow_request(&cx, &linen).map(|_| ())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("riley already follows linen");
|
println!("riley already follows linen");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
println!("\nPosts on the instance:");
|
println!("\nPosts on the instance:");
|
||||||
for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
|
for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
|
||||||
let Author { ref handle, .. } = post.author;
|
let Author { ref handle, .. } = post.author;
|
||||||
let content = post.content.content.as_ref().unwrap();
|
let content = post.content.content.as_ref().unwrap();
|
||||||
println!("- {:?} by {handle}:\n{content}", post.id)
|
println!("- {:?} by {handle}:\n{content}", post.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.run(|tx| {
|
||||||
println!("\nLinen's followers:");
|
println!("\nLinen's followers:");
|
||||||
for id in linen.followers(&cx).try_collect::<Vec<_>>()? {
|
for id in linen.followers(&tx).try_collect::<Vec<_>>()? {
|
||||||
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||||
println!("- @{account_name} ({id})");
|
println!("- @{account_name} ({id})");
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\nRiley's following:");
|
println!("\nRiley's following:");
|
||||||
for id in riley.following(&cx).try_collect::<Vec<_>>()? {
|
for id in riley.following(&tx).try_collect::<Vec<_>>()? {
|
||||||
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||||
println!("- @{account_name} ({id})");
|
println!("- @{account_name} ({id})");
|
||||||
}
|
}
|
||||||
|
@ -61,7 +75,7 @@ fn main() -> puppy::Result<()> {
|
||||||
if false {
|
if false {
|
||||||
println!("Biting riley");
|
println!("Biting riley");
|
||||||
linen.do_bite(&cx, &riley)?;
|
linen.do_bite(&cx, &riley)?;
|
||||||
for Bite { id, biter, .. } in riley.bites_suffered(&cx).try_collect::<Vec<_>>()? {
|
for Bite { id, biter, .. } in riley.bites_suffered(&tx).try_collect::<Vec<_>>()? {
|
||||||
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
|
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
|
||||||
println!("riley was bitten by @{account_name} at {}", id.timestamp());
|
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<Actor> {
|
fn get_or_create_actor(cx: &Context, username: &str) -> puppy::Result<Actor> {
|
||||||
let user = Actor::by_username(cx, username)?;
|
let user = cx.run(|tx| Actor::by_username(tx, username))?;
|
||||||
match user {
|
match user {
|
||||||
Some(key) => {
|
Some(key) => {
|
||||||
println!("found '{username}' ({key:?})");
|
println!("found '{username}' ({key:?})");
|
||||||
|
|
|
@ -11,13 +11,13 @@ pub mod ap {
|
||||||
config::Config,
|
config::Config,
|
||||||
data::{Id, PrivateKey, PublicKey},
|
data::{Id, PrivateKey, PublicKey},
|
||||||
fetch::{signatures::Private, SigningKey},
|
fetch::{signatures::Private, SigningKey},
|
||||||
get_local_ap_object, Key,
|
get_local_ap_object, Context, Key,
|
||||||
};
|
};
|
||||||
use serde_json::{to_string, Value};
|
use serde_json::{to_string, Value};
|
||||||
use crate::{respond, Response};
|
use crate::{respond, Response};
|
||||||
|
|
||||||
/// Proxy a request through the instance.
|
/// 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.
|
// Extract our query parameters.
|
||||||
let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else {
|
let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else {
|
||||||
return respond(400, Some("Expected `user` query param"), []);
|
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).
|
// 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 Ok(signing_key) = cx.run(|tx| {
|
||||||
let actor = Actor::by_username(&cx, user)?.unwrap();
|
let actor = Actor::by_username(&tx, user)?.unwrap();
|
||||||
let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) =
|
let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) =
|
||||||
cx.store().get_mixin_many(actor.key)?;
|
tx.get_mixin_many(actor.key)?;
|
||||||
let Id(owner) = cx.store().get_alias(actor.key)?.unwrap();
|
let Id(owner) = tx.get_alias(actor.key)?.unwrap();
|
||||||
let inner = Private::decode_pem(&key_pem);
|
let inner = Private::decode_pem(&key_pem);
|
||||||
SigningKey { id: key_id, owner, inner }
|
Ok(SigningKey { id: key_id, owner, inner })
|
||||||
})
|
}) else {
|
||||||
.unwrap();
|
panic!("failed to get signing key");
|
||||||
|
};
|
||||||
|
|
||||||
eprintln!("proxy: params: {params:?}");
|
eprintln!("proxy: params: {params:?}");
|
||||||
// Proxy the request through our fetcher.
|
// Proxy the request through our fetcher.
|
||||||
|
@ -47,16 +48,16 @@ pub mod ap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle POSTs to actor inboxes. Requires request signature.
|
/// 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!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve an ActivityPub object as json-ld.
|
/// 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::<Key>() else {
|
let Ok(parsed) = object_ulid.parse::<Key>() else {
|
||||||
return respond(400, Some("improperly formatted id"), []);
|
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 {
|
let Ok(object) = result else {
|
||||||
return respond(404, <Option<String>>::None, []);
|
return respond(404, <Option<String>>::None, []);
|
||||||
};
|
};
|
||||||
|
@ -67,8 +68,8 @@ pub mod ap {
|
||||||
const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json");
|
const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json");
|
||||||
|
|
||||||
/// Serve the special actor used for signing requests.
|
/// Serve the special actor used for signing requests.
|
||||||
pub fn serve_verifier_actor(cfg: Config) -> Response {
|
pub fn serve_verifier_actor(cx: &Context) -> Response {
|
||||||
let body = Verifier::load(&cfg).to_json_ld();
|
let body = cx.verifier().to_json_ld();
|
||||||
let encoded = serde_json::to_vec(&body).unwrap();
|
let encoded = serde_json::to_vec(&body).unwrap();
|
||||||
respond(200, Some(encoded), [AP_CONTENT_TYPE])
|
respond(200, Some(encoded), [AP_CONTENT_TYPE])
|
||||||
}
|
}
|
||||||
|
@ -77,26 +78,31 @@ pub mod ap {
|
||||||
pub mod wf {
|
pub mod wf {
|
||||||
//! WebFinger endpoints and related stuff.
|
//! WebFinger endpoints and related stuff.
|
||||||
|
|
||||||
use puppy::{config::Config, data::Username, Error};
|
use puppy::{data::Username, Context, Error};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{respond, Response};
|
use crate::{respond, Response};
|
||||||
|
|
||||||
const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json");
|
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) {
|
match query.iter().find_map(get_handle) {
|
||||||
// Serve JRDs for local actors.
|
// Serve JRDs for local actors.
|
||||||
Some(handle) if cfg.wf_domain == handle.instance => {
|
Some(handle) if cx.config().wf_domain == handle.instance => {
|
||||||
let id = puppy::context::<_, Error>(|cx| try {
|
let id = {
|
||||||
let user = cx
|
let user = cx
|
||||||
.store()
|
.store()
|
||||||
.lookup(Username(handle.username.to_string()))?
|
.lookup(Username(handle.username.to_string()))
|
||||||
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let id = cx.store().get_alias::<puppy::data::Id>(user)?.unwrap().0;
|
let id = cx
|
||||||
|
.store()
|
||||||
|
.get_alias::<puppy::data::Id>(user)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
id
|
id
|
||||||
})
|
};
|
||||||
.unwrap();
|
|
||||||
let jrd = json!({
|
let jrd = json!({
|
||||||
"subject": format!("acct:{}@{}", handle.username, handle.instance),
|
"subject": format!("acct:{}@{}", handle.username, handle.instance),
|
||||||
"links": [
|
"links": [
|
||||||
|
|
|
@ -3,16 +3,15 @@
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use http::request::Parts;
|
|
||||||
use http_body_util::{BodyExt as _, Full};
|
use http_body_util::{BodyExt as _, Full};
|
||||||
use hyper::body::Bytes;
|
use hyper::body::Bytes;
|
||||||
use hyper::server::conn::http1;
|
use hyper::server::conn::http1;
|
||||||
use hyper::service::service_fn;
|
use hyper::service::service_fn;
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
use puppy::auth::{SigError, Signer};
|
use puppy::auth::{self, SigError, Signer, Verifier};
|
||||||
use puppy::{auth::verify_signature, config::Config};
|
use puppy::store::Store;
|
||||||
use puppy::fetch::signatures::Signature;
|
use puppy::{auth::verify_signature, config::Config, Context};
|
||||||
use serde_json::{from_slice, json, Value};
|
use serde_json::{from_slice, Value};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -20,13 +19,17 @@ async fn main() {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
ap_domain: "test.piss-on.me".to_string(),
|
ap_domain: "test.piss-on.me".to_string(),
|
||||||
wf_domain: "test.piss-on.me".to_string(),
|
wf_domain: "test.piss-on.me".to_string(),
|
||||||
|
state_dir: "state".to_string(),
|
||||||
port: 1312,
|
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<dyn std::error::Error + Send + Sync>> {
|
pub async fn start(context: Context) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], cfg.port));
|
let addr = SocketAddr::from(([127, 0, 0, 1], context.config().port));
|
||||||
let listener = TcpListener::bind(addr).await?;
|
let listener = TcpListener::bind(addr).await?;
|
||||||
|
|
||||||
// We start a loop to continuously accept incoming connections
|
// We start a loop to continuously accept incoming connections
|
||||||
|
@ -37,13 +40,13 @@ pub async fn start(cfg: &Config) -> Result<(), Box<dyn std::error::Error + Send
|
||||||
// `hyper::rt` IO traits.
|
// `hyper::rt` IO traits.
|
||||||
let io = TokioIo::new(stream);
|
let io = TokioIo::new(stream);
|
||||||
|
|
||||||
let cfg = cfg.clone();
|
let context = context.clone();
|
||||||
// Spawn a tokio task to serve multiple connections concurrently
|
// Spawn a tokio task to serve multiple connections concurrently
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
// Finally, we bind the incoming connection to our `hello` service
|
// Finally, we bind the incoming connection to our `hello` service
|
||||||
if let Err(err) = http1::Builder::new()
|
if let Err(err) = http1::Builder::new()
|
||||||
// `service_fn` converts our function in a `Service`
|
// `service_fn` converts our function in a `Service`
|
||||||
.serve_connection(io, service_fn(|req| handle(req, cfg.clone())))
|
.serve_connection(io, service_fn(|req| handle(req, context.clone())))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
eprintln!("Error serving connection: {:?}", err);
|
eprintln!("Error serving connection: {:?}", err);
|
||||||
|
@ -55,45 +58,6 @@ pub async fn start(cfg: &Config) -> Result<(), Box<dyn std::error::Error + Send
|
||||||
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>;
|
||||||
|
|
||||||
/// The request handler.
|
|
||||||
async fn handle(req: Request, cfg: Config) -> Result<Response, Infallible> {
|
|
||||||
// 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.
|
// A parsed HTTP request for easy handling.
|
||||||
struct Req<'a> {
|
struct Req<'a> {
|
||||||
method: &'a Method,
|
method: &'a Method,
|
||||||
|
@ -109,7 +73,7 @@ impl Req<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_req<'x>(r: &'x http::Request<Bytes>) -> Req<'x> {
|
fn simplify<'x>(r: &'x http::Request<Bytes>) -> Req<'x> {
|
||||||
let path: Vec<&str> = r
|
let path: Vec<&str> = r
|
||||||
.uri()
|
.uri()
|
||||||
.path()
|
.path()
|
||||||
|
@ -136,37 +100,69 @@ use hyper::Method;
|
||||||
const POST: &Method = &Method::POST;
|
const POST: &Method = &Method::POST;
|
||||||
const GET: &Method = &Method::GET;
|
const GET: &Method = &Method::GET;
|
||||||
|
|
||||||
|
/// The request handler.
|
||||||
|
async fn handle(req: Request, cx: Context) -> Result<Response, Infallible> {
|
||||||
|
// 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.
|
/// Handle a signed and verified request.
|
||||||
///
|
///
|
||||||
/// This function is where all requests to a protected endpoint have to go through. If the 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
|
/// was signed but does not target a protected endpoint, this function will fall back to the
|
||||||
/// [`dispatch_public`] handler.
|
/// [`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");
|
eprintln!("Dispatching signed request");
|
||||||
match (req.method, req.path()) {
|
match (req.method, req.path()) {
|
||||||
// Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch".
|
// 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
|
// The one exception for this is `/s/request-verifier`, which is where the request
|
||||||
// verification actor lives.
|
// 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.
|
// POSTs to an actor's inbox need to be signed to prevent impersonation.
|
||||||
(POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| {
|
(POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| {
|
||||||
// We only handle the intermediate parsing of the json, full resolution of the
|
// We only handle the intermediate parsing of the json, full resolution of the
|
||||||
// activity object will happen inside the inbox handler itself.
|
// 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.
|
// 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
|
/// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the
|
||||||
/// function will return a 404 response.
|
/// 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");
|
eprintln!("Dispatching public request");
|
||||||
match (req.method, req.path()) {
|
match (req.method, req.path()) {
|
||||||
(GET, ["proxy"]) => api::ap::proxy(&req.params).await,
|
(GET, ["proxy"]) => api::ap::proxy(&cx, &req.params).await,
|
||||||
(GET, [".well-known", "webfinger"]) => api::wf::resolve(&req.params, cfg),
|
(GET, [".well-known", "webfinger"]) => api::wf::resolve(&cx, &req.params),
|
||||||
(GET, ["s", "request-verifier"]) => api::ap::serve_verifier_actor(cfg),
|
(GET, auth::VERIFIER_MOUNT) => api::ap::serve_verifier_actor(&cx),
|
||||||
(m, p) => {
|
(m, p) => {
|
||||||
eprintln!("404: {m} {p:?}");
|
eprintln!("404: {m} {p:?}");
|
||||||
error::not_found()
|
error::not_found()
|
||||||
|
|
|
@ -99,22 +99,12 @@ pub struct Private(rsa::RsaPrivateKey);
|
||||||
impl Private {
|
impl Private {
|
||||||
/// Generate a new keypair.
|
/// Generate a new keypair.
|
||||||
pub fn gen() -> (Private, Public) {
|
pub fn gen() -> (Private, Public) {
|
||||||
println!("[!] gen");
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let bits = 512;
|
let bits = 512;
|
||||||
let private = RsaPrivateKey::new(&mut rng, bits).unwrap();
|
let private = RsaPrivateKey::new(&mut rng, bits).unwrap();
|
||||||
let public = private.to_public_key();
|
let public = private.to_public_key();
|
||||||
(Private(private), Public(public))
|
(Private(private), Public(public))
|
||||||
}
|
}
|
||||||
pub fn tee(path: impl AsRef<Path>) -> (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.
|
/// Get the public counterpart to this key.
|
||||||
pub fn get_public(&self) -> Public {
|
pub fn get_public(&self) -> Public {
|
||||||
Public(self.0.to_public_key())
|
Public(self.0.to_public_key())
|
||||||
|
@ -312,7 +302,8 @@ fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, Strin
|
||||||
Ok(buf)
|
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);
|
const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5);
|
||||||
|
|
||||||
/// Configuration for the behavior of the signing and verification routines.
|
/// Configuration for the behavior of the signing and verification routines.
|
||||||
|
|
|
@ -1,75 +1,55 @@
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use store::{Key, Store, Transaction};
|
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.
|
/// The context of a running ActivityPuppy.
|
||||||
///
|
///
|
||||||
/// This type provides access to the data store and configuration.
|
/// This type provides access to the data store and configuration.
|
||||||
pub struct Context<'c> {
|
#[derive(Clone)]
|
||||||
pub(crate) config: &'c Config,
|
pub struct Context {
|
||||||
pub(crate) db: Store,
|
verifier: Option<Verifier>,
|
||||||
|
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].
|
/// Do a data store [transaction][store::Transaction].
|
||||||
pub fn run<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> {
|
pub fn run<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> {
|
||||||
self.db.run(f)
|
self.store.run(f)
|
||||||
}
|
}
|
||||||
/// Access the store directly.
|
/// Access the store directly.
|
||||||
pub fn store(&self) -> &Store {
|
pub fn store(&self) -> &Store {
|
||||||
&self.db
|
&self.store
|
||||||
}
|
}
|
||||||
/// Access the configuration.
|
/// Access the configuration.
|
||||||
pub fn config(&self) -> &Config {
|
pub fn config(&self) -> &Config {
|
||||||
self.config
|
&self.config
|
||||||
}
|
}
|
||||||
/// Create an ActivityPub object ID from a key.
|
/// Create an ActivityPub object ID from a key.
|
||||||
pub fn mk_url(&self, key: Key) -> String {
|
pub fn mk_url(&self, key: Key) -> String {
|
||||||
format!("https://{}/o/{key}", self.config.ap_domain)
|
format!("https://{}/o/{key}", self.config.ap_domain)
|
||||||
}
|
}
|
||||||
/// Get the verification actor.
|
/// Get the verification actor.
|
||||||
pub fn verifier(&self) -> &auth::Verifier {
|
pub fn verifier(&self) -> &Verifier {
|
||||||
todo!()
|
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<Store> = 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<T, E>(f: impl FnOnce(Context<'_>) -> Result<T, E>) -> Result<T, E>
|
|
||||||
where
|
|
||||||
E: From<Error>,
|
|
||||||
{
|
|
||||||
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.
|
/// Load a context for running tests in.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn test_context<T>(
|
pub fn test_context<T>(
|
||||||
config: Config,
|
config: Config,
|
||||||
schema: Schema,
|
schema: store::types::Schema,
|
||||||
test: impl FnOnce(Context<'_>) -> Result<T>,
|
test: impl FnOnce(Context) -> Result<T>,
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
Store::test(schema, |db| test(Context { config: &config, db }))
|
Store::test(schema, |store| {
|
||||||
|
test(Context { config, store, verifier: None })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//! Interactions between actors.
|
//! Interactions between actors.
|
||||||
|
|
||||||
use store::{util::IterExt as _, Key, StoreError};
|
use store::{util::IterExt as _, Key, StoreError, Transaction};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actor::Actor,
|
actor::Actor,
|
||||||
|
@ -29,15 +29,13 @@ impl Actor {
|
||||||
/// Makes `biter` bite `victim` and inserts the records into the database.
|
/// Makes `biter` bite `victim` and inserts the records into the database.
|
||||||
pub fn do_bite(&self, cx: &Context, victim: &Actor) -> Result<Bite> {
|
pub fn do_bite(&self, cx: &Context, victim: &Actor) -> Result<Bite> {
|
||||||
let bite = self.bite(victim);
|
let bite = self.bite(victim);
|
||||||
cx.run(|tx| {
|
cx.run(|tx| try { tx.create(bite) })?;
|
||||||
tx.create(bite)?;
|
Ok(bite)
|
||||||
Ok(bite)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/// Creates a follow request from `self` to `target`.
|
/// Creates a follow request from `self` to `target`.
|
||||||
pub fn do_follow_request(&self, cx: &Context, target: &Actor) -> Result<FollowRequest> {
|
pub fn do_follow_request(&self, cx: &Context, target: &Actor) -> Result<FollowRequest> {
|
||||||
|
let req = self.follow_request(target);
|
||||||
cx.run(|tx| {
|
cx.run(|tx| {
|
||||||
let req = self.follow_request(target);
|
|
||||||
tx.create(req)?;
|
tx.create(req)?;
|
||||||
tx.add_mixin(req.id, Status::Pending)?;
|
tx.add_mixin(req.id, Status::Pending)?;
|
||||||
Ok(req)
|
Ok(req)
|
||||||
|
@ -72,49 +70,48 @@ impl Actor {
|
||||||
self.key == req.target,
|
self.key == req.target,
|
||||||
"only the target of a follow request may accept it"
|
"only the target of a follow request may accept it"
|
||||||
};
|
};
|
||||||
cx.run(|tx| try {
|
cx.run(|tx| try { tx.update(req.id, |_| Status::Rejected) })?;
|
||||||
tx.update(req.id, |_| Status::Rejected)?;
|
Ok(())
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/// Get all pending follow request for `self`.
|
/// Get all pending follow request for `self`.
|
||||||
pub fn pending_requests<'c>(
|
pub fn pending_requests<'c>(
|
||||||
&self,
|
&self,
|
||||||
cx: &'c Context,
|
tx: &'c Transaction<'c>,
|
||||||
) -> impl Iterator<Item = Result<FollowRequest>> + 'c {
|
) -> impl Iterator<Item = Result<FollowRequest>> + 'c {
|
||||||
cx.store()
|
tx.incoming::<FollowRequest>(self.key)
|
||||||
.incoming::<FollowRequest>(self.key)
|
|
||||||
.map_err(Error::Store)
|
.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.
|
/// Get all nodes `self` is following.
|
||||||
pub fn following<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Key>> + 'c {
|
pub fn following<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator<Item = Result<Key>> + 'c {
|
||||||
cx.store()
|
tx.outgoing::<Follows>(self.key)
|
||||||
.outgoing::<Follows>(self.key)
|
|
||||||
.map_err(Error::Store)
|
.map_err(Error::Store)
|
||||||
.map_ok(|a| a.followed)
|
.map_ok(|a| a.followed)
|
||||||
}
|
}
|
||||||
/// Get all followers of `self`.
|
/// Get all followers of `self`.
|
||||||
pub fn followers<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Key>> + 'c {
|
pub fn followers<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator<Item = Result<Key>> + 'c {
|
||||||
cx.store()
|
tx.incoming::<Follows>(self.key)
|
||||||
.incoming::<Follows>(self.key)
|
|
||||||
.map_err(Error::Store)
|
.map_err(Error::Store)
|
||||||
.map_ok(|a| a.follower)
|
.map_ok(|a| a.follower)
|
||||||
}
|
}
|
||||||
/// List all specific times `self` was bitten.
|
/// List all specific times `self` was bitten.
|
||||||
pub fn bites_suffered<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Bite>> + 'c {
|
pub fn bites_suffered<'c>(
|
||||||
cx.store().incoming::<Bite>(self.key).map_err(Error::Store)
|
&self,
|
||||||
|
tx: &'c Transaction<'c>,
|
||||||
|
) -> impl Iterator<Item = Result<Bite>> + 'c {
|
||||||
|
tx.incoming::<Bite>(self.key).map_err(Error::Store)
|
||||||
}
|
}
|
||||||
/// Check whether `self` follows `other`.
|
/// Check whether `self` follows `other`.
|
||||||
pub fn follows(&self, cx: &Context, other: &Actor) -> Result<bool> {
|
pub fn follows(&self, tx: &Transaction<'_>, other: &Actor) -> Result<bool> {
|
||||||
try { cx.db.exists::<Follows>(self.key, other.key)? }
|
try { tx.exists::<Follows>(self.key, other.key)? }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FollowRequest {
|
impl FollowRequest {
|
||||||
/// Determine if this follow request is pending.
|
/// Determine if this follow request is pending.
|
||||||
pub fn is_pending(&self, cx: &Context) -> Result<bool> {
|
pub fn is_pending(&self, tx: &Transaction<'_>) -> Result<bool> {
|
||||||
// The status is stored as a mixin, so we need to get it.
|
// The status is stored as a mixin, so we need to get it.
|
||||||
let Some(st) = cx.db.get_mixin::<Status>(self.id)? else {
|
let Some(st) = tx.get_mixin::<Status>(self.id)? else {
|
||||||
// If we don't have a status for a follow request, something is borked.
|
// If we don't have a status for a follow request, something is borked.
|
||||||
return Err(StoreError::Missing.into());
|
return Err(StoreError::Missing.into());
|
||||||
};
|
};
|
||||||
|
@ -122,7 +119,7 @@ impl FollowRequest {
|
||||||
// relation already exists.
|
// relation already exists.
|
||||||
debug_assert! {
|
debug_assert! {
|
||||||
!(st == Status::Pending)
|
!(st == Status::Pending)
|
||||||
|| cx.db.exists::<Follows>(self.origin, self.target).map(|x| !x)?,
|
|| tx.exists::<Follows>(self.origin, self.target).map(|x| !x)?,
|
||||||
"fr.is_pending -> !(fr.origin follows fr.target)"
|
"fr.is_pending -> !(fr.origin follows fr.target)"
|
||||||
};
|
};
|
||||||
Ok(st == Status::Pending)
|
Ok(st == Status::Pending)
|
||||||
|
@ -150,28 +147,31 @@ mod tests {
|
||||||
Config {
|
Config {
|
||||||
ap_domain: String::from("unit-test.puppy.gay"),
|
ap_domain: String::from("unit-test.puppy.gay"),
|
||||||
wf_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]
|
#[test]
|
||||||
fn create_fr() -> Result<()> {
|
fn create_fr() -> Result<()> {
|
||||||
test_context(test_config(), schema(), |cx| try {
|
test_context(test_config(), schema(), |cx| {
|
||||||
let db = cx.store();
|
|
||||||
let (alice, bob) = make_test_actors(&cx)?;
|
let (alice, bob) = make_test_actors(&cx)?;
|
||||||
alice.do_follow_request(&cx, &bob)?;
|
alice.do_follow_request(&cx, &bob)?;
|
||||||
assert!(
|
assert!(
|
||||||
db.exists::<FollowRequest>(alice.key, bob.key)?,
|
cx.store().exists::<FollowRequest>(alice.key, bob.key)?,
|
||||||
"(alice -> bob) ∈ follow-requested"
|
"(alice -> bob) ∈ follow-requested"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!db.exists::<Follows>(alice.key, bob.key)?,
|
!cx.store().exists::<Follows>(alice.key, bob.key)?,
|
||||||
"(alice -> bob) ∉ follows"
|
"(alice -> bob) ∉ follows"
|
||||||
);
|
);
|
||||||
let pending_for_bob = bob
|
let pending_for_bob = cx.run(|tx| {
|
||||||
.pending_requests(&cx)
|
bob.pending_requests(&tx)
|
||||||
.map_ok(|fr| fr.origin)
|
.map_ok(|fr| fr.origin)
|
||||||
.try_collect::<Vec<_>>()?;
|
.try_collect::<Vec<_>>()
|
||||||
|
})?;
|
||||||
assert_eq!(pending_for_bob, vec![alice.key], "bob.pending = {{alice}}");
|
assert_eq!(pending_for_bob, vec![alice.key], "bob.pending = {{alice}}");
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,16 +192,17 @@ mod tests {
|
||||||
"(bob -> alice) ∉ follows"
|
"(bob -> alice) ∉ follows"
|
||||||
);
|
);
|
||||||
|
|
||||||
let pending_for_bob: Vec<_> = bob.pending_requests(&cx).try_collect()?;
|
cx.run(|tx| try {
|
||||||
assert!(pending_for_bob.is_empty(), "bob.pending = ∅");
|
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()?;
|
let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
followers_of_bob,
|
followers_of_bob,
|
||||||
vec![alice.key],
|
vec![alice.key],
|
||||||
"bob.followers = {{alice}}"
|
"bob.followers = {{alice}}"
|
||||||
);
|
);
|
||||||
Ok(())
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,19 +213,21 @@ mod tests {
|
||||||
let req = alice.do_follow_request(&cx, &bob)?;
|
let req = alice.do_follow_request(&cx, &bob)?;
|
||||||
bob.do_accept_request(&cx, req)?;
|
bob.do_accept_request(&cx, req)?;
|
||||||
|
|
||||||
let followers_of_bob: Vec<_> = bob.followers(&cx).try_collect()?;
|
cx.run(|tx| try {
|
||||||
assert_eq!(
|
let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?;
|
||||||
followers_of_bob,
|
assert_eq!(
|
||||||
vec![alice.key],
|
followers_of_bob,
|
||||||
"bob.followers = {{alice}}"
|
vec![alice.key],
|
||||||
);
|
"bob.followers = {{alice}}"
|
||||||
|
);
|
||||||
|
|
||||||
let following_of_alice: Vec<_> = alice.following(&cx).try_collect()?;
|
let following_of_alice: Vec<_> = alice.following(&tx).try_collect()?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
following_of_alice,
|
following_of_alice,
|
||||||
vec![bob.key],
|
vec![bob.key],
|
||||||
"alice.following = {{bob}}"
|
"alice.following = {{bob}}"
|
||||||
);
|
);
|
||||||
|
})?
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,13 @@
|
||||||
// but that would make every type signature ever 100x more complicated, so we're not doing it.
|
// but that would make every type signature ever 100x more complicated, so we're not doing it.
|
||||||
#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
|
#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
|
||||||
pub use context::{context, Context};
|
pub use context::Context;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use context::test_context;
|
pub use context::test_context;
|
||||||
|
|
||||||
use data::{ActivityKind, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey};
|
use data::{ActivityKind, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey};
|
||||||
|
|
||||||
|
use store::Transaction;
|
||||||
pub use store::{self, Key, StoreError};
|
pub use store::{self, Key, StoreError};
|
||||||
pub use fetch;
|
pub use fetch;
|
||||||
|
|
||||||
|
@ -30,21 +31,21 @@ mod interact;
|
||||||
/// Retrieve an ActivityPub object from the database.
|
/// Retrieve an ActivityPub object from the database.
|
||||||
///
|
///
|
||||||
/// Fails with `Error::Missing` if the required properties are not present.
|
/// Fails with `Error::Missing` if the required properties are not present.
|
||||||
pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result<fetch::Object> {
|
pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::Object> {
|
||||||
let Some(obj) = cx.db.get_mixin::<Object>(key)? else {
|
let Some(obj) = tx.get_mixin::<Object>(key)? else {
|
||||||
// We need this data in order to determine the object type. If the passed key does not
|
// 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.
|
// have this data, it must not be an ActivityPub object.
|
||||||
return Err(Error::MissingData { node: key, prop: "Object" });
|
return Err(Error::MissingData { node: key, prop: "Object" });
|
||||||
};
|
};
|
||||||
match obj.kind {
|
match obj.kind {
|
||||||
ObjectKind::Actor => {
|
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" });
|
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" });
|
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" });
|
return Err(Error::MissingData { node: key, prop: "PublicKey" });
|
||||||
};
|
};
|
||||||
Ok(fetch::Object::Actor(fetch::Actor {
|
Ok(fetch::Object::Actor(fetch::Actor {
|
||||||
|
@ -60,19 +61,19 @@ pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result<fetch::Object>
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ObjectKind::Activity(ActivityKind::Create) => {
|
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`");
|
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 {
|
Ok(fetch::Object::Activity(fetch::Activity {
|
||||||
id: obj.id.0.into(),
|
id: obj.id.0.into(),
|
||||||
actor: actor.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"),
|
kind: String::from("Create"),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ObjectKind::Notelike(kind) => {
|
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!()
|
panic!()
|
||||||
};
|
};
|
||||||
Ok(fetch::Object::Object {
|
Ok(fetch::Object::Object {
|
||||||
|
@ -105,9 +106,8 @@ pub mod actor {
|
||||||
|
|
||||||
impl Actor {
|
impl Actor {
|
||||||
/// Get a local actor from the store by their username.
|
/// Get a local actor from the store by their username.
|
||||||
pub fn by_username(cx: &Context, username: impl ToString) -> Result<Option<Actor>> {
|
pub fn by_username(tx: &Transaction<'_>, username: impl ToString) -> Result<Option<Actor>> {
|
||||||
let maybe_key = cx
|
let maybe_key = tx
|
||||||
.store()
|
|
||||||
.lookup(Username(username.to_string()))
|
.lookup(Username(username.to_string()))
|
||||||
.map_err(Error::Store)?;
|
.map_err(Error::Store)?;
|
||||||
// For now, we only have local actors.
|
// For now, we only have local actors.
|
||||||
|
@ -125,8 +125,8 @@ pub mod actor {
|
||||||
cx.run(|tx| {
|
cx.run(|tx| {
|
||||||
let username: Username = username.to_string().into();
|
let username: Username = username.to_string().into();
|
||||||
// Federation stuff
|
// Federation stuff
|
||||||
mixin_ap_actor(tx, key, &cx.config.ap_domain, true)?;
|
mixin_ap_actor(tx, key, &cx.config().ap_domain, true)?;
|
||||||
mixin_priv_key(tx, key, &cx.config.ap_domain)?;
|
mixin_priv_key(tx, key, &cx.config().ap_domain)?;
|
||||||
// Social properties
|
// Social properties
|
||||||
tx.add_alias(key, username.clone())?;
|
tx.add_alias(key, username.clone())?;
|
||||||
tx.add_mixin(key, Profile {
|
tx.add_mixin(key, Profile {
|
||||||
|
@ -195,6 +195,7 @@ pub mod config {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub ap_domain: String,
|
pub ap_domain: String,
|
||||||
pub wf_domain: String,
|
pub wf_domain: String,
|
||||||
|
pub state_dir: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,8 +204,10 @@ pub mod auth {
|
||||||
use fetch::signatures::{Private, Public, Signature};
|
use fetch::signatures::{Private, Public, Signature};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::{config::Config, Context};
|
||||||
|
|
||||||
|
/// Checks request signatures.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Verifier {
|
pub struct Verifier {
|
||||||
actor_id: String,
|
actor_id: String,
|
||||||
key_id: String,
|
key_id: String,
|
||||||
|
@ -212,13 +215,20 @@ pub mod auth {
|
||||||
public: Public,
|
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 {
|
impl Verifier {
|
||||||
pub async fn get_public_key(&self, uri: &str) -> Result<fetch::Key, String> {
|
/// 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<fetch::Key<Public>, String> {
|
||||||
let json = fetch::resolve(&self.signing_key(), uri).await.unwrap();
|
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 {
|
fetch::Key {
|
||||||
id: self.key_id.clone(),
|
id: self.key_id.clone(),
|
||||||
owner: self.actor_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 {
|
pub fn to_json_ld(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"@context": [
|
"@context": [
|
||||||
|
@ -243,14 +254,15 @@ pub mod auth {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the actor's verifier actor.
|
||||||
pub fn load(cfg: &Config) -> Verifier {
|
pub fn load(cfg: &Config) -> Verifier {
|
||||||
println!("[*] loading private key");
|
println!("[*] loading private key");
|
||||||
let domain = &cfg.ap_domain;
|
let Config { ap_domain, state_dir, .. } = cfg;
|
||||||
let private = Private::load(".state/fetcher.pem");
|
let private = Private::load(format!("{state_dir}/fetcher.pem"));
|
||||||
println!("* done loading private key");
|
println!("* done loading private key");
|
||||||
Verifier {
|
Verifier {
|
||||||
actor_id: format!("https://{domain}/s/request-verifier"),
|
actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"),
|
||||||
key_id: format!("https://{domain}/s/request-verifier#sig-key"),
|
key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"),
|
||||||
public: private.get_public(),
|
public: private.get_public(),
|
||||||
private,
|
private,
|
||||||
}
|
}
|
||||||
|
@ -269,21 +281,22 @@ pub mod auth {
|
||||||
VerificationFailed { error: String },
|
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.
|
/// Check the signature for a request.
|
||||||
pub async fn verify_signature(
|
pub async fn verify_signature(
|
||||||
|
cx: &Context,
|
||||||
req: &fetch::http::Request<impl AsRef<[u8]> + std::fmt::Debug>,
|
req: &fetch::http::Request<impl AsRef<[u8]> + std::fmt::Debug>,
|
||||||
cfg: &Config,
|
|
||||||
) -> Result<Option<Signer>, SigError> {
|
) -> Result<Option<Signer>, SigError> {
|
||||||
println!(">>> starting signature verification for {req:#?}");
|
println!(">>> starting signature verification for {req:#?}");
|
||||||
|
|
||||||
if req.uri().path() == "/s/request-verifier" {
|
if req.uri().path() == VERIFIER_PATH {
|
||||||
// Allow access to the request verifier actor without checking the signature.
|
// HACK: Allow access to the request verifier actor without checking the signature.
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
println!(">>> not going for the verifier!");
|
println!(">>> not going for the verifier!");
|
||||||
|
|
||||||
if req.headers().get("signature").is_none() {
|
if req.headers().get("signature").is_none() {
|
||||||
// Request is not signed!
|
// Request is not signed.
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
println!(">>> has signature");
|
println!(">>> has signature");
|
||||||
|
@ -299,8 +312,8 @@ pub mod auth {
|
||||||
println!(">>> signature is syntatically valid");
|
println!(">>> signature is syntatically valid");
|
||||||
|
|
||||||
// Fetch the public key using the verifier private key.
|
// Fetch the public key using the verifier private key.
|
||||||
let verifier = Verifier::load(cfg);
|
let verifier = cx.verifier();
|
||||||
let Ok(public_key) = verifier.get_public_key(sig.key_id()).await else {
|
let Ok(public_key) = verifier.fetch_public_key(sig.key_id()).await else {
|
||||||
return Err(SigError::FailedToFetchKey {
|
return Err(SigError::FailedToFetchKey {
|
||||||
keyid: sig.key_id().to_string(),
|
keyid: sig.key_id().to_string(),
|
||||||
});
|
});
|
||||||
|
@ -308,14 +321,12 @@ pub mod auth {
|
||||||
println!(">>> public key fetched");
|
println!(">>> public key fetched");
|
||||||
|
|
||||||
// Verify the signature header on the request.
|
// Verify the signature header on the request.
|
||||||
let public_key = public_key.upgrade();
|
|
||||||
println!(">>> upgraded");
|
|
||||||
if let Err(error) = public_key.verify(&sig) {
|
if let Err(error) = public_key.verify(&sig) {
|
||||||
println!(">>> rejected");
|
println!(">>> rejected");
|
||||||
Err(SigError::VerificationFailed { error })
|
Err(SigError::VerificationFailed { error })
|
||||||
} else {
|
} else {
|
||||||
println!(">>> request verified");
|
println!(">>> request verified");
|
||||||
Ok(Some(Signer { ap_id: public_key.owner.into() }))
|
Ok(Some(Signer { ap_id: public_key.owner }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,53 +17,6 @@ pub struct Post {
|
||||||
pub key: Key,
|
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::<AuthorOf>(self.key)
|
|
||||||
.map_ok(|a| a.author)
|
|
||||||
.next()
|
|
||||||
.unwrap()?;
|
|
||||||
let author_id = cx.db.get_alias::<data::Id>(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 {
|
impl From<&str> for Content {
|
||||||
fn from(value: &str) -> Self {
|
fn from(value: &str) -> Self {
|
||||||
value.to_string().into()
|
value.to_string().into()
|
||||||
|
@ -180,7 +133,7 @@ pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> cr
|
||||||
// Local stuff
|
// Local stuff
|
||||||
mixin_post(tx, key, author, content)?;
|
mixin_post(tx, key, author, content)?;
|
||||||
// Federation stuff
|
// 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_alias(key, id.clone())?;
|
||||||
tx.add_mixin(key, Object {
|
tx.add_mixin(key, Object {
|
||||||
kind: ObjectKind::Notelike("Note".to_string()),
|
kind: ObjectKind::Notelike("Note".to_string()),
|
||||||
|
|
|
@ -217,6 +217,18 @@ impl Transaction<'_> {
|
||||||
{
|
{
|
||||||
op::between::<A>(self, a, b).map_ok(A::from)
|
op::between::<A>(self, a, b).map_ok(A::from)
|
||||||
}
|
}
|
||||||
|
/// Construct the arrow from its identifier.
|
||||||
|
pub fn get_arrow<A>(&self, key: Key) -> Result<Option<A>>
|
||||||
|
where
|
||||||
|
A: Arrow<Kind = Multi>,
|
||||||
|
{
|
||||||
|
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 {
|
impl Batch {
|
||||||
|
|
|
@ -128,6 +128,13 @@ impl Transaction<'_> {
|
||||||
{
|
{
|
||||||
op::join_on(self, iter.into_iter().map_ok(f))
|
op::join_on(self, iter.into_iter().map_ok(f))
|
||||||
}
|
}
|
||||||
|
/// Get multiple mixins associated with the same key.
|
||||||
|
pub fn get_mixin_many<T>(&self, key: Key) -> Result<T>
|
||||||
|
where
|
||||||
|
T: GetMany,
|
||||||
|
{
|
||||||
|
T::get(self, key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Batch {
|
impl Batch {
|
||||||
|
|
Loading…
Reference in a new issue