A whole bunch of different refactors

This commit is contained in:
Riley Apeldoorn 2024-05-01 18:47:02 +02:00
parent 09cf289b75
commit 8d350e8cd9
10 changed files with 273 additions and 300 deletions

View file

@ -1,16 +1,25 @@
#![feature(iterator_try_collect)]
use puppy::{
actor::Actor,
data::{Bite, Profile},
auth::Verifier,
config::Config,
data::{schema, Bite, Profile},
post::Author,
store::util::IterExt as _,
store::{util::IterExt as _, Store},
Context,
};
fn main() -> puppy::Result<()> {
// puppy::store::Store::nuke(".state")?;
puppy::context(|cx| {
let db = cx.store();
let config = Config {
ap_domain: "test.piss-on.me".to_string(),
wf_domain: "test.piss-on.me".to_string(),
state_dir: ".state".to_string(),
port: 1312,
};
let verifier = Verifier::load(&config);
let db = Store::open(&config.state_dir, schema())?;
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")?;
@ -22,21 +31,25 @@ fn main() -> puppy::Result<()> {
if true {
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");
if let Some(req) = linen
.pending_requests(&cx)
.pending_requests(&tx)
.find_ok(|r| r.origin == riley.key)?
{
println!("accepting the pending follow request");
linen.do_accept_request(&cx, req)?;
linen.do_accept_request(&cx, req)
} else {
println!("no pending follow request; creating");
riley.do_follow_request(&cx, &linen)?;
riley.do_follow_request(&cx, &linen).map(|_| ())
}
} else {
println!("riley already follows linen");
Ok(())
}
})?;
}
println!("\nPosts on the instance:");
@ -46,14 +59,15 @@ fn main() -> puppy::Result<()> {
println!("- {:?} by {handle}:\n{content}", post.id)
}
cx.run(|tx| {
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();
println!("- @{account_name} ({id})");
}
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();
println!("- @{account_name} ({id})");
}
@ -61,7 +75,7 @@ fn main() -> puppy::Result<()> {
if false {
println!("Biting 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();
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> {
let user = Actor::by_username(cx, username)?;
fn get_or_create_actor(cx: &Context, username: &str) -> puppy::Result<Actor> {
let user = cx.run(|tx| Actor::by_username(tx, username))?;
match user {
Some(key) => {
println!("found '{username}' ({key:?})");

View file

@ -11,13 +11,13 @@ pub mod ap {
config::Config,
data::{Id, PrivateKey, PublicKey},
fetch::{signatures::Private, SigningKey},
get_local_ap_object, Key,
get_local_ap_object, Context, Key,
};
use serde_json::{to_string, Value};
use crate::{respond, Response};
/// 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.
let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else {
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).
let signing_key = puppy::context::<_, puppy::Error>(|cx| try {
let actor = Actor::by_username(&cx, user)?.unwrap();
let Ok(signing_key) = cx.run(|tx| {
let actor = Actor::by_username(&tx, user)?.unwrap();
let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) =
cx.store().get_mixin_many(actor.key)?;
let Id(owner) = cx.store().get_alias(actor.key)?.unwrap();
tx.get_mixin_many(actor.key)?;
let Id(owner) = tx.get_alias(actor.key)?.unwrap();
let inner = Private::decode_pem(&key_pem);
SigningKey { id: key_id, owner, inner }
})
.unwrap();
Ok(SigningKey { id: key_id, owner, inner })
}) else {
panic!("failed to get signing key");
};
eprintln!("proxy: params: {params:?}");
// Proxy the request through our fetcher.
@ -47,16 +48,16 @@ pub mod ap {
}
/// 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!()
}
/// 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 {
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 {
return respond(404, <Option<String>>::None, []);
};
@ -67,8 +68,8 @@ pub mod ap {
const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json");
/// Serve the special actor used for signing requests.
pub fn serve_verifier_actor(cfg: Config) -> Response {
let body = Verifier::load(&cfg).to_json_ld();
pub fn serve_verifier_actor(cx: &Context) -> Response {
let body = cx.verifier().to_json_ld();
let encoded = serde_json::to_vec(&body).unwrap();
respond(200, Some(encoded), [AP_CONTENT_TYPE])
}
@ -77,26 +78,31 @@ pub mod ap {
pub mod wf {
//! WebFinger endpoints and related stuff.
use puppy::{config::Config, data::Username, Error};
use puppy::{data::Username, Context, Error};
use serde_json::json;
use crate::{respond, Response};
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) {
// Serve JRDs for local actors.
Some(handle) if cfg.wf_domain == handle.instance => {
let id = puppy::context::<_, Error>(|cx| try {
Some(handle) if cx.config().wf_domain == handle.instance => {
let id = {
let user = cx
.store()
.lookup(Username(handle.username.to_string()))?
.lookup(Username(handle.username.to_string()))
.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
})
.unwrap();
};
let jrd = json!({
"subject": format!("acct:{}@{}", handle.username, handle.instance),
"links": [

View file

@ -3,16 +3,15 @@
use std::convert::Infallible;
use std::net::SocketAddr;
use http::request::Parts;
use http_body_util::{BodyExt as _, Full};
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use puppy::auth::{SigError, Signer};
use puppy::{auth::verify_signature, config::Config};
use puppy::fetch::signatures::Signature;
use serde_json::{from_slice, json, Value};
use puppy::auth::{self, SigError, Signer, Verifier};
use puppy::store::Store;
use puppy::{auth::verify_signature, config::Config, Context};
use serde_json::{from_slice, Value};
use tokio::net::TcpListener;
#[tokio::main]
@ -20,13 +19,17 @@ async fn main() {
let config = Config {
ap_domain: "test.piss-on.me".to_string(),
wf_domain: "test.piss-on.me".to_string(),
state_dir: "state".to_string(),
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>> {
let addr = SocketAddr::from(([127, 0, 0, 1], cfg.port));
pub async fn start(context: Context) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([127, 0, 0, 1], context.config().port));
let listener = TcpListener::bind(addr).await?;
// 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.
let io = TokioIo::new(stream);
let cfg = cfg.clone();
let context = context.clone();
// Spawn a tokio task to serve multiple connections concurrently
tokio::task::spawn(async move {
// Finally, we bind the incoming connection to our `hello` service
if let Err(err) = http1::Builder::new()
// `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
{
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 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.
struct Req<'a> {
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
.uri()
.path()
@ -136,37 +100,69 @@ use hyper::Method;
const POST: &Method = &Method::POST;
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.
///
/// 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
/// [`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");
match (req.method, req.path()) {
// 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
// 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.
(POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| {
// We only handle the intermediate parsing of the json, full resolution of the
// 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.
_ => dispatch_public(req, cfg).await,
_ => dispatch_public(cx, req).await,
}
}
/// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the
/// 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");
match (req.method, req.path()) {
(GET, ["proxy"]) => api::ap::proxy(&req.params).await,
(GET, [".well-known", "webfinger"]) => api::wf::resolve(&req.params, cfg),
(GET, ["s", "request-verifier"]) => api::ap::serve_verifier_actor(cfg),
(GET, ["proxy"]) => api::ap::proxy(&cx, &req.params).await,
(GET, [".well-known", "webfinger"]) => api::wf::resolve(&cx, &req.params),
(GET, auth::VERIFIER_MOUNT) => api::ap::serve_verifier_actor(&cx),
(m, p) => {
eprintln!("404: {m} {p:?}");
error::not_found()

View file

@ -99,22 +99,12 @@ pub struct Private(rsa::RsaPrivateKey);
impl Private {
/// Generate a new keypair.
pub fn gen() -> (Private, Public) {
println!("[!] gen");
let mut rng = rand::thread_rng();
let bits = 512;
let private = RsaPrivateKey::new(&mut rng, bits).unwrap();
let public = private.to_public_key();
(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.
pub fn get_public(&self) -> Public {
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)
}
/// 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);
/// Configuration for the behavior of the signing and verification routines.

View file

@ -1,75 +1,55 @@
use std::sync::OnceLock;
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.
///
/// This type provides access to the data store and configuration.
pub struct Context<'c> {
pub(crate) config: &'c Config,
pub(crate) db: Store,
#[derive(Clone)]
pub struct Context {
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].
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.
pub fn store(&self) -> &Store {
&self.db
&self.store
}
/// Access the configuration.
pub fn config(&self) -> &Config {
self.config
&self.config
}
/// Create an ActivityPub object ID from a key.
pub fn mk_url(&self, key: Key) -> String {
format!("https://{}/o/{key}", self.config.ap_domain)
}
/// Get the verification actor.
pub fn verifier(&self) -> &auth::Verifier {
todo!()
pub fn verifier(&self) -> &Verifier {
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.
#[cfg(test)]
pub fn test_context<T>(
config: Config,
schema: Schema,
test: impl FnOnce(Context<'_>) -> Result<T>,
schema: store::types::Schema,
test: impl FnOnce(Context) -> Result<T>,
) -> Result<T> {
Store::test(schema, |db| test(Context { config: &config, db }))
Store::test(schema, |store| {
test(Context { config, store, verifier: None })
})
}

View file

@ -1,6 +1,6 @@
//! Interactions between actors.
use store::{util::IterExt as _, Key, StoreError};
use store::{util::IterExt as _, Key, StoreError, Transaction};
use crate::{
actor::Actor,
@ -29,15 +29,13 @@ impl Actor {
/// Makes `biter` bite `victim` and inserts the records into the database.
pub fn do_bite(&self, cx: &Context, victim: &Actor) -> Result<Bite> {
let bite = self.bite(victim);
cx.run(|tx| {
tx.create(bite)?;
cx.run(|tx| try { tx.create(bite) })?;
Ok(bite)
})
}
/// Creates a follow request from `self` to `target`.
pub fn do_follow_request(&self, cx: &Context, target: &Actor) -> Result<FollowRequest> {
cx.run(|tx| {
let req = self.follow_request(target);
cx.run(|tx| {
tx.create(req)?;
tx.add_mixin(req.id, Status::Pending)?;
Ok(req)
@ -72,49 +70,48 @@ impl Actor {
self.key == req.target,
"only the target of a follow request may accept it"
};
cx.run(|tx| try {
tx.update(req.id, |_| Status::Rejected)?;
})
cx.run(|tx| try { tx.update(req.id, |_| Status::Rejected) })?;
Ok(())
}
/// Get all pending follow request for `self`.
pub fn pending_requests<'c>(
&self,
cx: &'c Context,
tx: &'c Transaction<'c>,
) -> impl Iterator<Item = Result<FollowRequest>> + 'c {
cx.store()
.incoming::<FollowRequest>(self.key)
tx.incoming::<FollowRequest>(self.key)
.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.
pub fn following<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Key>> + 'c {
cx.store()
.outgoing::<Follows>(self.key)
pub fn following<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator<Item = Result<Key>> + 'c {
tx.outgoing::<Follows>(self.key)
.map_err(Error::Store)
.map_ok(|a| a.followed)
}
/// Get all followers of `self`.
pub fn followers<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Key>> + 'c {
cx.store()
.incoming::<Follows>(self.key)
pub fn followers<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator<Item = Result<Key>> + 'c {
tx.incoming::<Follows>(self.key)
.map_err(Error::Store)
.map_ok(|a| a.follower)
}
/// List all specific times `self` was bitten.
pub fn bites_suffered<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Bite>> + 'c {
cx.store().incoming::<Bite>(self.key).map_err(Error::Store)
pub fn bites_suffered<'c>(
&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`.
pub fn follows(&self, cx: &Context, other: &Actor) -> Result<bool> {
try { cx.db.exists::<Follows>(self.key, other.key)? }
pub fn follows(&self, tx: &Transaction<'_>, other: &Actor) -> Result<bool> {
try { tx.exists::<Follows>(self.key, other.key)? }
}
}
impl FollowRequest {
/// 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.
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.
return Err(StoreError::Missing.into());
};
@ -122,7 +119,7 @@ impl FollowRequest {
// relation already exists.
debug_assert! {
!(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)"
};
Ok(st == Status::Pending)
@ -150,28 +147,31 @@ mod tests {
Config {
ap_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]
fn create_fr() -> Result<()> {
test_context(test_config(), schema(), |cx| try {
let db = cx.store();
test_context(test_config(), schema(), |cx| {
let (alice, bob) = make_test_actors(&cx)?;
alice.do_follow_request(&cx, &bob)?;
assert!(
db.exists::<FollowRequest>(alice.key, bob.key)?,
cx.store().exists::<FollowRequest>(alice.key, bob.key)?,
"(alice -> bob) ∈ follow-requested"
);
assert!(
!db.exists::<Follows>(alice.key, bob.key)?,
!cx.store().exists::<Follows>(alice.key, bob.key)?,
"(alice -> bob) ∉ follows"
);
let pending_for_bob = bob
.pending_requests(&cx)
let pending_for_bob = cx.run(|tx| {
bob.pending_requests(&tx)
.map_ok(|fr| fr.origin)
.try_collect::<Vec<_>>()?;
.try_collect::<Vec<_>>()
})?;
assert_eq!(pending_for_bob, vec![alice.key], "bob.pending = {{alice}}");
Ok(())
})
}
@ -192,16 +192,17 @@ mod tests {
"(bob -> alice) ∉ follows"
);
let pending_for_bob: Vec<_> = bob.pending_requests(&cx).try_collect()?;
cx.run(|tx| try {
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!(
followers_of_bob,
vec![alice.key],
"bob.followers = {{alice}}"
);
Ok(())
})
})
}
@ -212,19 +213,21 @@ mod tests {
let req = alice.do_follow_request(&cx, &bob)?;
bob.do_accept_request(&cx, req)?;
let followers_of_bob: Vec<_> = bob.followers(&cx).try_collect()?;
cx.run(|tx| try {
let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?;
assert_eq!(
followers_of_bob,
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!(
following_of_alice,
vec![bob.key],
"alice.following = {{bob}}"
);
})?
})
}
}

View file

@ -12,12 +12,13 @@
// but that would make every type signature ever 100x more complicated, so we're not doing it.
#![deny(clippy::disallowed_methods, clippy::disallowed_types)]
pub use context::{context, Context};
pub use context::Context;
#[cfg(test)]
pub use context::test_context;
use data::{ActivityKind, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey};
use store::Transaction;
pub use store::{self, Key, StoreError};
pub use fetch;
@ -30,21 +31,21 @@ mod interact;
/// Retrieve an ActivityPub object from the database.
///
/// Fails with `Error::Missing` if the required properties are not present.
pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result<fetch::Object> {
let Some(obj) = cx.db.get_mixin::<Object>(key)? else {
pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::Object> {
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
// have this data, it must not be an ActivityPub object.
return Err(Error::MissingData { node: key, prop: "Object" });
};
match obj.kind {
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" });
};
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" });
};
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" });
};
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) => {
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`");
};
let Id(actor) = cx.db.get_alias(actor)?.unwrap();
let Id(actor) = tx.get_alias(actor)?.unwrap();
Ok(fetch::Object::Activity(fetch::Activity {
id: obj.id.0.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"),
}))
}
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!()
};
Ok(fetch::Object::Object {
@ -105,9 +106,8 @@ pub mod actor {
impl Actor {
/// Get a local actor from the store by their username.
pub fn by_username(cx: &Context, username: impl ToString) -> Result<Option<Actor>> {
let maybe_key = cx
.store()
pub fn by_username(tx: &Transaction<'_>, username: impl ToString) -> Result<Option<Actor>> {
let maybe_key = tx
.lookup(Username(username.to_string()))
.map_err(Error::Store)?;
// For now, we only have local actors.
@ -125,8 +125,8 @@ pub mod actor {
cx.run(|tx| {
let username: Username = username.to_string().into();
// Federation stuff
mixin_ap_actor(tx, key, &cx.config.ap_domain, true)?;
mixin_priv_key(tx, key, &cx.config.ap_domain)?;
mixin_ap_actor(tx, key, &cx.config().ap_domain, true)?;
mixin_priv_key(tx, key, &cx.config().ap_domain)?;
// Social properties
tx.add_alias(key, username.clone())?;
tx.add_mixin(key, Profile {
@ -195,6 +195,7 @@ pub mod config {
pub struct Config {
pub ap_domain: String,
pub wf_domain: String,
pub state_dir: String,
pub port: u16,
}
}
@ -203,8 +204,10 @@ pub mod auth {
use fetch::signatures::{Private, Public, Signature};
use serde_json::{json, Value};
use crate::config::Config;
use crate::{config::Config, Context};
/// Checks request signatures.
#[derive(Clone)]
pub struct Verifier {
actor_id: String,
key_id: String,
@ -212,13 +215,20 @@ pub mod auth {
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 {
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();
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 {
id: self.key_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 {
json!({
"@context": [
@ -243,14 +254,15 @@ pub mod auth {
})
}
/// Load the actor's verifier actor.
pub fn load(cfg: &Config) -> Verifier {
println!("[*] loading private key");
let domain = &cfg.ap_domain;
let private = Private::load(".state/fetcher.pem");
let Config { ap_domain, state_dir, .. } = cfg;
let private = Private::load(format!("{state_dir}/fetcher.pem"));
println!("* done loading private key");
Verifier {
actor_id: format!("https://{domain}/s/request-verifier"),
key_id: format!("https://{domain}/s/request-verifier#sig-key"),
actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"),
key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"),
public: private.get_public(),
private,
}
@ -269,21 +281,22 @@ pub mod auth {
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.
pub async fn verify_signature(
cx: &Context,
req: &fetch::http::Request<impl AsRef<[u8]> + std::fmt::Debug>,
cfg: &Config,
) -> Result<Option<Signer>, SigError> {
println!(">>> starting signature verification for {req:#?}");
if req.uri().path() == "/s/request-verifier" {
// Allow access to the request verifier actor without checking the signature.
if req.uri().path() == VERIFIER_PATH {
// HACK: Allow access to the request verifier actor without checking the signature.
return Ok(None);
}
println!(">>> not going for the verifier!");
if req.headers().get("signature").is_none() {
// Request is not signed!
// Request is not signed.
return Ok(None);
};
println!(">>> has signature");
@ -299,8 +312,8 @@ pub mod auth {
println!(">>> signature is syntatically valid");
// Fetch the public key using the verifier private key.
let verifier = Verifier::load(cfg);
let Ok(public_key) = verifier.get_public_key(sig.key_id()).await else {
let verifier = cx.verifier();
let Ok(public_key) = verifier.fetch_public_key(sig.key_id()).await else {
return Err(SigError::FailedToFetchKey {
keyid: sig.key_id().to_string(),
});
@ -308,14 +321,12 @@ pub mod auth {
println!(">>> public key fetched");
// Verify the signature header on the request.
let public_key = public_key.upgrade();
println!(">>> upgraded");
if let Err(error) = public_key.verify(&sig) {
println!(">>> rejected");
Err(SigError::VerificationFailed { error })
} else {
println!(">>> request verified");
Ok(Some(Signer { ap_id: public_key.owner.into() }))
Ok(Some(Signer { ap_id: public_key.owner }))
}
}
}

View file

@ -17,53 +17,6 @@ pub struct Post {
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 {
fn from(value: &str) -> Self {
value.to_string().into()
@ -180,7 +133,7 @@ pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> cr
// Local stuff
mixin_post(tx, key, author, content)?;
// 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_mixin(key, Object {
kind: ObjectKind::Notelike("Note".to_string()),

View file

@ -217,6 +217,18 @@ impl Transaction<'_> {
{
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 {

View file

@ -128,6 +128,13 @@ impl Transaction<'_> {
{
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 {