2024-04-13 00:44:53 +02:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
|
|
|
use base64::Engine;
|
|
|
|
use http_signature_normalization::Config;
|
|
|
|
use openssl::{hash::MessageDigest, pkey::{PKey, Private}, sign::Signer};
|
|
|
|
use reqwest::{header::{CONTENT_TYPE, USER_AGENT}, Method};
|
2024-03-25 21:19:02 +01:00
|
|
|
use sea_orm::{DatabaseConnection, EntityTrait, IntoActiveModel};
|
|
|
|
|
2024-04-13 03:03:54 +02:00
|
|
|
use crate::{model, VERSION};
|
2024-03-25 21:19:02 +01:00
|
|
|
|
2024-04-13 00:44:53 +02:00
|
|
|
use super::Context;
|
|
|
|
|
2024-03-25 21:19:02 +01:00
|
|
|
|
|
|
|
pub struct Fetcher {
|
|
|
|
db: DatabaseConnection,
|
2024-04-13 00:44:53 +02:00
|
|
|
key: PKey<Private>, // TODO store pre-parsed
|
2024-03-25 21:19:02 +01:00
|
|
|
domain: String, // TODO merge directly with Context so we don't need to copy this
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Fetcher {
|
|
|
|
pub fn new(db: DatabaseConnection, domain: String, key: String) -> Self {
|
2024-04-13 00:44:53 +02:00
|
|
|
Fetcher { db, domain, key: PKey::private_key_from_pem(key.as_bytes()).unwrap() }
|
2024-03-25 21:19:02 +01:00
|
|
|
}
|
|
|
|
|
2024-04-13 00:44:53 +02:00
|
|
|
pub async fn request<T: serde::de::DeserializeOwned>(
|
|
|
|
method: reqwest::Method,
|
|
|
|
url: &str,
|
|
|
|
payload: Option<&str>,
|
|
|
|
from: &str,
|
|
|
|
key: &PKey<Private>,
|
|
|
|
domain: &str,
|
|
|
|
) -> reqwest::Result<T> {
|
|
|
|
let host = Context::server(url);
|
|
|
|
let date = chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(); // lmao @ "GMT"
|
|
|
|
let path = url.replace("https://", "").replace("http://", "").replace(&host, "");
|
|
|
|
let mut headers : BTreeMap<String, String> = [
|
|
|
|
("Host".to_string(), host.clone()),
|
|
|
|
("Date".to_string(), date.clone()),
|
|
|
|
].into();
|
|
|
|
|
2024-04-13 02:35:18 +02:00
|
|
|
let mut client = reqwest::Client::new()
|
|
|
|
.request(method, url)
|
|
|
|
.header(CONTENT_TYPE, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")
|
|
|
|
.header(USER_AGENT, format!("upub+{VERSION} ({domain})"))
|
2024-04-13 02:55:10 +02:00
|
|
|
.header("Host", host.clone())
|
|
|
|
.header("Date", date.clone());
|
2024-04-13 00:44:53 +02:00
|
|
|
|
2024-04-13 03:03:54 +02:00
|
|
|
let mut signature_cfg = Config::new().mastodon_compat();
|
2024-04-13 02:55:10 +02:00
|
|
|
let mut to_sign_raw = format!("(request-target): post {path}\nhost: {host}\ndate: {date}");
|
|
|
|
let mut headers_to_inspect = "(request-target) host date";
|
2024-04-13 00:44:53 +02:00
|
|
|
|
|
|
|
if let Some(payload) = payload {
|
|
|
|
let digest = format!("sha-256={}", base64::prelude::BASE64_STANDARD.encode(openssl::sha::sha256(payload.as_bytes())));
|
2024-04-13 02:55:10 +02:00
|
|
|
to_sign_raw = format!("(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}");
|
|
|
|
headers_to_inspect = "(request-target) host date digest";
|
2024-04-13 00:44:53 +02:00
|
|
|
headers.insert("Digest".to_string(), digest.clone());
|
|
|
|
signature_cfg = signature_cfg.require_header("digest");
|
|
|
|
client = client
|
|
|
|
.header("Digest", digest)
|
|
|
|
.body(payload.to_string());
|
2024-03-25 21:19:02 +01:00
|
|
|
}
|
|
|
|
|
2024-04-13 02:55:10 +02:00
|
|
|
let signature_header_lib = signature_cfg
|
2024-04-13 00:44:53 +02:00
|
|
|
.begin_sign("POST", &path, headers)
|
|
|
|
.unwrap()
|
|
|
|
.sign(format!("{from}#main-key"), |to_sign| {
|
2024-04-13 02:55:10 +02:00
|
|
|
tracing::info!("signature string:\nlib>> {to_sign}\nraw>> {to_sign_raw}");
|
2024-04-13 00:44:53 +02:00
|
|
|
let mut signer = Signer::new(MessageDigest::sha256(), key)?;
|
|
|
|
signer.update(to_sign.as_bytes())?;
|
|
|
|
let signature = base64::prelude::BASE64_URL_SAFE.encode(signer.sign_to_vec()?);
|
|
|
|
Ok(signature) as crate::Result<_>
|
|
|
|
})
|
|
|
|
.unwrap()
|
2024-04-13 03:03:54 +02:00
|
|
|
.signature_header()
|
|
|
|
.replace("hs2019", "rsa-sha256"); // TODO what the fuck??? why isn't this customizable???
|
2024-04-13 00:44:53 +02:00
|
|
|
|
2024-04-13 02:55:10 +02:00
|
|
|
let signature_header = {
|
|
|
|
let mut signer = Signer::new(MessageDigest::sha256(), key).unwrap();
|
|
|
|
signer.update(to_sign_raw.as_bytes()).unwrap();
|
|
|
|
let signature = base64::prelude::BASE64_STANDARD.encode(signer.sign_to_vec().unwrap());
|
|
|
|
format!("keyId=\"{from}#main-key\",algorithm=\"rsa-sha256\",headers=\"{headers_to_inspect}\",signature=\"{signature}\"")
|
|
|
|
};
|
|
|
|
|
|
|
|
tracing::info!("signature headers:\nlib>> {signature_header_lib}\nraw>> {signature_header}");
|
|
|
|
|
2024-04-13 00:44:53 +02:00
|
|
|
client
|
|
|
|
.header("Signature", signature_header)
|
2024-03-25 21:19:02 +01:00
|
|
|
.send()
|
|
|
|
.await?
|
2024-04-13 00:44:53 +02:00
|
|
|
.error_for_status()?
|
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-04-13 01:49:04 +02:00
|
|
|
pub async fn user(&self, id: &str) -> crate::Result<model::user::Model> {
|
2024-04-13 00:44:53 +02:00
|
|
|
if let Some(x) = model::user::Entity::find_by_id(id).one(&self.db).await? {
|
|
|
|
return Ok(x); // already in db, easy
|
|
|
|
}
|
2024-03-25 21:19:02 +01:00
|
|
|
|
2024-04-13 00:44:53 +02:00
|
|
|
let user = Self::request::<serde_json::Value>(
|
|
|
|
Method::GET, id, None, &format!("https://{}", self.domain), &self.key, &self.domain,
|
|
|
|
).await?;
|
2024-03-25 21:19:02 +01:00
|
|
|
let user_model = model::user::Model::new(&user)?;
|
|
|
|
|
|
|
|
model::user::Entity::insert(user_model.clone().into_active_model())
|
|
|
|
.exec(&self.db).await?;
|
|
|
|
|
|
|
|
Ok(user_model)
|
|
|
|
}
|
2024-04-13 00:44:53 +02:00
|
|
|
|
2024-04-13 01:49:04 +02:00
|
|
|
pub async fn activity(&self, id: &str) -> crate::Result<model::activity::Model> {
|
2024-04-13 00:44:53 +02:00
|
|
|
if let Some(x) = model::activity::Entity::find_by_id(id).one(&self.db).await? {
|
|
|
|
return Ok(x); // already in db, easy
|
|
|
|
}
|
|
|
|
|
|
|
|
let activity = Self::request::<serde_json::Value>(
|
|
|
|
Method::GET, id, None, &format!("https://{}", self.domain), &self.key, &self.domain,
|
|
|
|
).await?;
|
|
|
|
let activity_model = model::activity::Model::new(&activity)?;
|
|
|
|
|
|
|
|
model::activity::Entity::insert(activity_model.clone().into_active_model())
|
|
|
|
.exec(&self.db).await?;
|
|
|
|
|
|
|
|
Ok(activity_model)
|
|
|
|
}
|
|
|
|
|
2024-04-13 01:49:04 +02:00
|
|
|
pub async fn object(&self, id: &str) -> crate::Result<model::object::Model> {
|
2024-04-13 00:44:53 +02:00
|
|
|
if let Some(x) = model::object::Entity::find_by_id(id).one(&self.db).await? {
|
|
|
|
return Ok(x); // already in db, easy
|
|
|
|
}
|
|
|
|
|
|
|
|
let object = Self::request::<serde_json::Value>(
|
|
|
|
Method::GET, id, None, &format!("https://{}", self.domain), &self.key, &self.domain,
|
|
|
|
).await?;
|
|
|
|
let object_model = model::object::Model::new(&object)?;
|
|
|
|
|
|
|
|
model::object::Entity::insert(object_model.clone().into_active_model())
|
|
|
|
.exec(&self.db).await?;
|
|
|
|
|
|
|
|
Ok(object_model)
|
|
|
|
}
|
2024-03-25 21:19:02 +01:00
|
|
|
}
|