v6/src/activitypub/federate.rs

91 lines
3.0 KiB
Rust

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<ActorExt> {
let actor = CLIENT
.get(id)
.header("Accept", "application/activity+json, application/json")
.send()
.await?
.json::<ActorExt>()
.await?;
Ok(actor)
}
pub async fn sign_and_send<Kind: serde::Serialize>(
activity: &ActorAndObject<Kind>,
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))
}
}