use super::{actor, keys, types::ActorExt, CLIENT}; use activitystreams::activity::ActorAndObject; use anyhow::anyhow; use chrono::Utc; use http_signature_normalization::Config; use hyper::header::CONTENT_TYPE; use log::{debug, error, info}; use openssl::{hash::MessageDigest, pkey::PKey, sha::Sha256, sign::Signer}; use reqwest::header::{ACCEPT, DATE, HOST}; use std::collections::BTreeMap; use url::Url; pub async fn fetch_actor(id: &str) -> anyhow::Result { let actor = CLIENT .get(id) .header("Accept", "application/activity+json, application/json") .send() .await? .json::() .await?; Ok(actor) } pub async fn sign_and_send( activity: &ActorAndObject, inbox: &str, ) -> anyhow::Result<()> { if std::env::var("SKIP_FEDERATE").is_ok() { info!("Skipping federation"); return Ok(()); } let inbox_url = Url::parse(inbox)?; let body = serde_json::to_string(activity)?; let mut headers = BTreeMap::new(); let host = inbox_url .host_str() .ok_or(anyhow!("missing inbox host"))? .to_owned(); headers.insert("host".into(), host.clone()); // mastodon wants rfc 2616 dates, which are always in GMT // and GMT is the same as UTC so we just hardcode it in the format let date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(); headers.insert("date".into(), date.clone()); let mut sha256 = Sha256::new(); sha256.update(body.as_bytes()); let digest = base64::encode(sha256.finish()); let digest_header = format!("SHA-256={}", digest); headers.insert("digest".into(), digest_header.clone()); let config = Config::new().mastodon_compat(); let sig_header = config .begin_sign("POST", inbox_url.path(), headers)? .sign::<_, anyhow::Error>(actor::KEY_ID.to_string(), |signing_str| { debug!("Signing string: {}", signing_str); let keypair = PKey::private_key_from_pem(keys::PRIV_KEY_PEM.as_ref())?; let mut signer = Signer::new(MessageDigest::sha256(), &keypair)?; signer.update(signing_str.as_bytes())?; let sig = signer.sign_to_vec()?; Ok(base64::encode(&sig)) })? .signature_header(); debug!("Sending: {}\nSignature: {}", body, sig_header); let response = CLIENT .post(inbox_url) .body(body.into_bytes()) .header(HOST, host) .header(DATE, date) .header("digest", digest_header) .header("signature", sig_header) .header(ACCEPT, "application/activity+json, application/json") .header(CONTENT_TYPE, "application/activity+json") .send() .await?; if (200..299).contains(&response.status().as_u16()) { Ok(()) } else { let status = response.status(); if let Ok(s) = response.text().await { error!("Failing response body: {}", s); } Err(anyhow!("Unexpected status code: {}", status)) } }