refactor: everything

This commit is contained in:
nullishamy 2025-04-11 12:29:29 +01:00
parent 90577e43b0
commit 022e6f9c6d
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
32 changed files with 1570 additions and 668 deletions

192
ferri-main/src/ap/http.rs Normal file
View file

@ -0,0 +1,192 @@
use reqwest::{IntoUrl, Response};
use serde::Serialize;
use url::Url;
use rsa::{
RsaPrivateKey,
pkcs1v15::SigningKey,
pkcs8::DecodePrivateKey,
sha2::{Digest, Sha256},
signature::{RandomizedSigner, SignatureEncoding},
};
use base64::prelude::*;
use chrono::Utc;
pub struct HttpClient {
client: reqwest::Client,
}
#[derive(Debug)]
pub struct PostSignature {
date: String,
digest: String,
signature: String,
}
#[derive(Debug)]
struct GetSignature {
date: String,
signature: String,
}
enum RequestVerb {
GET,
POST,
}
pub struct RequestBuilder {
verb: RequestVerb,
url: Url,
body: String,
inner: reqwest::RequestBuilder,
}
impl RequestBuilder {
pub fn json(mut self, json: impl Serialize + Sized) -> RequestBuilder {
let body = serde_json::to_string(&json).unwrap();
self.inner = self.inner.body(body.clone());
self.body = body;
self
}
pub fn activity(mut self) -> RequestBuilder {
self.inner = self.inner
.header("Content-Type", "application/activity+json")
.header("Accept", "application/activity+json");
self
}
pub async fn send(self) -> Result<Response, reqwest::Error> {
dbg!(&self.inner);
self.inner.send().await
}
pub fn sign(mut self, key_id: &str) -> RequestBuilder {
match self.verb {
RequestVerb::GET => {
let sig = self.sign_get_request(key_id);
self.inner = self.inner
.header("Date", sig.date)
.header("Signature", sig.signature);
self
}
RequestVerb::POST => {
let sig = self.sign_post_request(key_id);
self.inner = self.inner
.header("Date", sig.date)
.header("Digest", sig.digest)
.header("Signature", sig.signature);
self
}
}
}
fn sign_get_request(&self, key_id: &str) -> GetSignature {
let url = &self.url;
let host = url.host_str().unwrap();
let path = url.path();
let private_key = RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap();
let signing_key = SigningKey::<Sha256>::new(private_key);
// UTC=GMT for our purposes, use it
// RFC7231 is hardcoded to use GMT for.. some reason
let ts = Utc::now();
// RFC7231 string
let date = ts.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
let to_sign = format!(
"(request-target): get {}\nhost: {}\ndate: {}",
path, host, date
);
let signature = signing_key.sign_with_rng(&mut rand::rngs::OsRng, &to_sign.into_bytes());
let header = format!(
"keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\",signature=\"{}\"",
key_id,
BASE64_STANDARD.encode(signature.to_bytes())
);
GetSignature {
date,
signature: header,
}
}
fn sign_post_request(&self, key_id: &str) -> PostSignature {
let body = &self.body;
let url = &self.url;
let host = url.host_str().unwrap();
let path = url.path();
let private_key = RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap();
let signing_key = SigningKey::<Sha256>::new(private_key);
let mut hasher = Sha256::new();
hasher.update(body);
let sha256 = hasher.finalize();
let b64 = BASE64_STANDARD.encode(sha256);
let digest = format!("SHA-256={}", b64);
// UTC=GMT for our purposes, use it
// RFC7231 is hardcoded to use GMT for.. some reason
let ts = Utc::now();
// RFC7231 string
let date = ts.format("%a, %d %b %Y %H:%M:%S GMT").to_string();
let to_sign = format!(
"(request-target): post {}\nhost: {}\ndate: {}\ndigest: {}",
path, host, date, digest
);
let signature = signing_key.sign_with_rng(&mut rand::rngs::OsRng, &to_sign.into_bytes());
let header = format!(
"keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{}\"",
key_id,
BASE64_STANDARD.encode(signature.to_bytes())
);
PostSignature {
date,
digest,
signature: header,
}
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}
impl HttpClient {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
pub fn get(&self, url: impl IntoUrl + Clone) -> RequestBuilder {
RequestBuilder {
verb: RequestVerb::GET,
url: url.clone().into_url().unwrap(),
body: String::new(),
inner: self.client.get(url),
}
}
pub fn post(&self, url: impl IntoUrl + Clone) -> RequestBuilder {
RequestBuilder {
verb: RequestVerb::POST,
url: url.clone().into_url().unwrap(),
body: String::new(),
inner: self.client.post(url),
}
}
}

View file

@ -1,27 +1,188 @@
pub type ObjectId = String;
use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use sqlx::Sqlite;
pub enum ObjectType {
Person,
}
pub struct Object {
id: ObjectId,
ty: ObjectType
}
pub mod http;
#[derive(Debug, Clone)]
pub struct Actor {
obj: Object,
inbox: Inbox,
outbox: Outbox,
id: String,
inbox: String,
outbox: String,
}
pub struct Inbox {}
#[derive(Debug, Clone)]
pub struct User {
id: String,
username: String,
actor: Actor,
display_name: String,
}
pub struct Outbox {}
impl User {
pub fn id(&self) -> &str {
&self.id
}
pub struct Message {}
pub fn username(&self) -> &str {
&self.username
}
pub fn actor_id(&self) -> &str {
&self.actor.id
}
pub fn display_name(&self) -> &str {
&self.display_name
}
pub fn actor(&self) -> &Actor {
&self.actor
}
pub async fn from_username(
username: &str,
conn: impl sqlx::Executor<'_, Database = Sqlite>,
) -> User {
let user = sqlx::query!(
r#"
SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox
FROM user u
INNER JOIN actor a ON u.actor_id = a.id
WHERE username = ?1
"#,
username
)
.fetch_one(conn)
.await
.unwrap();
User {
id: user.id,
username: user.username,
actor: Actor {
id: user.actor_own_id,
inbox: user.inbox,
outbox: user.outbox,
},
display_name: user.display_name,
}
}
pub async fn from_actor_id(
actor_id: &str,
conn: impl sqlx::Executor<'_, Database = Sqlite>,
) -> User {
let user = sqlx::query!(
r#"
SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox
FROM user u
INNER JOIN actor a ON u.actor_id = a.id
WHERE actor_id = ?1
"#,
actor_id
)
.fetch_one(conn)
.await
.unwrap();
User {
id: user.id,
username: user.username,
actor: Actor {
id: user.actor_own_id,
inbox: user.inbox,
outbox: user.outbox,
},
display_name: user.display_name,
}
}
}
#[derive(Debug, Clone)]
pub enum ActivityType {
Follow,
}
impl ActivityType {
fn to_raw(self) -> String {
match self {
ActivityType::Follow => "Follow".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct Activity {
pub id: String,
pub ty: ActivityType,
pub object: String,
pub published: DateTime<Local>,
}
pub type KeyId = String;
#[derive(Debug, Clone)]
pub struct OutgoingActivity {
pub signed_by: KeyId,
pub req: Activity,
pub to: Actor,
}
#[derive(Serialize, Deserialize, Debug)]
struct RawActivity {
#[serde(rename = "@context")]
#[serde(skip_deserializing)]
context: String,
id: String,
#[serde(rename = "type")]
ty: String,
actor: String,
object: String,
published: String,
}
type OutboxTransport = http::HttpClient;
pub struct Outbox<'a> {
user: User,
transport: &'a OutboxTransport,
}
impl<'a> Outbox<'a> {
pub fn user(&self) -> &User {
&self.user
}
pub async fn post(&self, activity: OutgoingActivity) {
dbg!(&activity);
let raw = RawActivity {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: activity.req.id,
ty: activity.req.ty.to_raw(),
actor: self.user.actor.id.clone(),
object: activity.req.object,
published: activity.req.published.to_rfc3339(),
};
dbg!(&raw);
let follow_res = self
.transport
.post(activity.to.inbox)
.activity()
.json(&raw)
.sign(&activity.signed_by)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
dbg!(follow_res);
}
pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> {
Outbox { user, transport }
}
}