mirror of
https://github.com/nullishamy/ferri.git
synced 2025-06-28 09:04:18 +00:00
refactor: everything
This commit is contained in:
parent
90577e43b0
commit
022e6f9c6d
32 changed files with 1570 additions and 668 deletions
192
ferri-main/src/ap/http.rs
Normal file
192
ferri-main/src/ap/http.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue