Compare commits

..

No commits in common. "main" and "v0.3.112-pre" have entirely different histories.

20 changed files with 791 additions and 1601 deletions

View File

@ -21,8 +21,7 @@ jobs:
- -
name: Clippy name: Clippy
run: | run: |
# cargo clippy --no-default-features -- -D warnings cargo clippy --no-default-features -- -D warnings
cargo clippy --no-default-features
tests: tests:
runs-on: docker runs-on: docker
@ -208,6 +207,7 @@ jobs:
direction: upload direction: upload
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
release-dir: artifacts/ release-dir: artifacts/
prerelease: true
publish-crate: publish-crate:
needs: [build] needs: [build]

2140
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ap-relay" name = "ap-relay"
description = "A simple activitypub relay" description = "A simple activitypub relay"
version = "0.3.116" version = "0.3.112-pre"
authors = ["asonix <asonix@asonix.dog>"] authors = ["asonix <asonix@asonix.dog>"]
license = "AGPL-3.0" license = "AGPL-3.0"
readme = "README.md" readme = "README.md"
@ -24,60 +24,59 @@ default = []
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = { version = "4.4.0", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls-0_23"] } actix-web = { version = "4.4.0", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls-0_22"] }
actix-webfinger = { version = "0.5.0", default-features = false } actix-webfinger = { version = "0.5.0", default-features = false }
activitystreams = "0.7.0-alpha.25" activitystreams = "0.7.0-alpha.25"
activitystreams-ext = "0.1.0-alpha.3" activitystreams-ext = "0.1.0-alpha.3"
ammonia = "4.0.0" ammonia = "4.0.0"
async-cpupool = "0.3.0" async-cpupool = { version = "0.2.0", git = "https://git.asonix.dog/safe-async/async-cpupool", branch = "asonix/replace-flume" }
bcrypt = "0.16" bcrypt = "0.15"
base64 = "0.22" base64 = "0.22"
clap = { version = "4.0.0", features = ["derive"] } clap = { version = "4.0.0", features = ["derive"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
config = { version = "0.14.0", default-features = false, features = ["toml", "json", "yaml"] } config = { version = "0.14.0", default-features = false, features = ["toml", "json", "yaml"] }
console-subscriber = { version = "0.4", optional = true } console-subscriber = { version = "0.2", optional = true }
dashmap = "6.0.1" dashmap = "5.1.0"
dotenv = "0.15.0" dotenv = "0.15.0"
futures-core = "0.3.30"
lru = "0.12.0" lru = "0.12.0"
metrics = "0.23.0" metrics = "0.22.0"
metrics-exporter-prometheus = { version = "0.15.0", default-features = false, features = [ metrics-exporter-prometheus = { version = "0.13.0", default-features = false, features = [
"http-listener", "http-listener",
] } ] }
metrics-util = "0.17.0" metrics-util = "0.16.0"
mime = "0.3.16" mime = "0.3.16"
minify-html = "0.15.0" minify-html = "0.15.0"
opentelemetry = "0.27.1" opentelemetry = "0.22"
opentelemetry_sdk = { version = "0.27", features = ["rt-tokio"] } opentelemetry_sdk = { version = "0.22", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.27", features = ["grpc-tonic"] } opentelemetry-otlp = "0.15"
pin-project-lite = "0.2.9" pin-project-lite = "0.2.9"
# pinned to metrics-util # pinned to metrics-util
quanta = "0.12.0" quanta = "0.12.0"
rand = "0.8" rand = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"]} reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "stream"]}
reqwest-middleware = { version = "0.4", default-features = false, features = ["json"] } reqwest-middleware = "0.2"
reqwest-tracing = "0.5.0" reqwest-tracing = "0.4.5"
ring = "0.17.5" ring = "0.17.5"
rsa = "0.9" rsa = "0.9"
rsa-magic-public-key = "0.8.0" rsa-magic-public-key = "0.8.0"
rustls = { version = "0.23.0", default-features = false, features = ["ring", "logging", "std", "tls12"] } rustls = "0.22.0"
rustls-channel-resolver = "0.3.0" rustls-channel-resolver = "0.2.0"
rustls-pemfile = "2" rustls-pemfile = "2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sled = "0.34.7" sled = "0.34.7"
streem = "0.2.0" streem = "0.2.0"
teloxide = { version = "0.13.0", default-features = false, features = [ teloxide = { version = "0.12.0", default-features = false, features = [
"ctrlc_handler", "ctrlc_handler",
"macros", "macros",
"rustls", "rustls",
] } ] }
thiserror = "2.0" thiserror = "1.0"
time = { version = "0.3.17", features = ["serde"] } time = { version = "0.3.17", features = ["serde"] }
tracing = "0.1" tracing = "0.1"
tracing-error = "0.2" tracing-error = "0.2"
tracing-log = "0.2" tracing-log = "0.2"
tracing-opentelemetry = "0.28" tracing-opentelemetry = "0.23"
tracing-subscriber = { version = "0.3", features = [ tracing-subscriber = { version = "0.3", features = [
"ansi", "ansi",
"env-filter", "env-filter",
@ -87,17 +86,17 @@ tokio = { version = "1", features = ["full", "tracing"] }
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
[dependencies.background-jobs] [dependencies.background-jobs]
version = "0.19.0" version = "0.18.0"
default-features = false default-features = false
features = ["error-logging", "metrics", "tokio"] features = ["error-logging", "metrics", "tokio"]
[dependencies.http-signature-normalization-actix] [dependencies.http-signature-normalization-actix]
version = "0.11.1" version = "0.11.0"
default-features = false default-features = false
features = ["server", "ring"] features = ["server", "ring"]
[dependencies.http-signature-normalization-reqwest] [dependencies.http-signature-normalization-reqwest]
version = "0.13.0" version = "0.11.0"
default-features = false default-features = false
features = ["middleware", "ring"] features = ["middleware", "ring"]

View File

@ -2,7 +2,7 @@ version: '3.3'
services: services:
relay: relay:
image: asonix/relay:0.3.115 image: asonix/relay:0.3.108
ports: ports:
- "8079:8079" - "8079:8079"
restart: always restart: always

View File

@ -20,16 +20,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1733550349, "lastModified": 1711163522,
"narHash": "sha256-NcGumB4Lr6KSDq+nIqXtNA8QwAQKDSZT7N9OTGWbTrs=", "narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e2605d0744c2417b09f8bf850dfca42fcf537d34", "rev": "44d0940ea560dee511026a53f0e2e2cde489b4d4",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-24.11", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@ -2,7 +2,7 @@
description = "relay"; description = "relay";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };

View File

@ -5,7 +5,7 @@
rustPlatform.buildRustPackage { rustPlatform.buildRustPackage {
pname = "relay"; pname = "relay";
version = "0.3.116"; version = "0.3.111";
src = ./.; src = ./.;
cargoLock.lockFile = ./Cargo.lock; cargoLock.lockFile = ./Cargo.lock;

View File

@ -5,6 +5,7 @@ use crate::{
error::{Error, ErrorKind}, error::{Error, ErrorKind},
extractors::XApiToken, extractors::XApiToken,
}; };
use actix_web::http::header::Header;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@ -86,17 +87,13 @@ async fn get_results<T: DeserializeOwned>(
let res = client let res = client
.get(iri.as_str()) .get(iri.as_str())
.header(XApiToken::http1_name(), x_api_token.to_string()) .header(XApiToken::name(), x_api_token.to_string())
.send() .send()
.await .await
.map_err(|e| ErrorKind::SendRequest(iri.to_string(), e.to_string()))?; .map_err(|e| ErrorKind::SendRequest(iri.to_string(), e.to_string()))?;
if !res.status().is_success() { if !res.status().is_success() {
return Err(ErrorKind::Status( return Err(ErrorKind::Status(iri.to_string(), res.status()).into());
iri.to_string(),
crate::http1::status_to_http02(res.status()),
)
.into());
} }
let t = res let t = res
@ -119,7 +116,7 @@ async fn post_domains(
let res = client let res = client
.post(iri.as_str()) .post(iri.as_str())
.header(XApiToken::http1_name(), x_api_token.to_string()) .header(XApiToken::name(), x_api_token.to_string())
.json(&Domains { domains }) .json(&Domains { domains })
.send() .send()
.await .await

View File

@ -605,8 +605,6 @@ impl Db {
pub(crate) async fn add_blocks(&self, domains: Vec<String>) -> Result<(), Error> { pub(crate) async fn add_blocks(&self, domains: Vec<String>) -> Result<(), Error> {
self.unblock(move |inner| { self.unblock(move |inner| {
let connected_by_domain = inner.connected_by_domain(&domains).collect::<Vec<_>>();
let res = ( let res = (
&inner.connected_actor_ids, &inner.connected_actor_ids,
&inner.blocked_domains, &inner.blocked_domains,
@ -617,7 +615,7 @@ impl Db {
let mut blocked_batch = Batch::default(); let mut blocked_batch = Batch::default();
let mut allowed_batch = Batch::default(); let mut allowed_batch = Batch::default();
for connected in &connected_by_domain { for connected in inner.connected_by_domain(&domains) {
connected_batch.remove(connected.as_str().as_bytes()); connected_batch.remove(connected.as_str().as_bytes());
} }
@ -689,11 +687,9 @@ impl Db {
pub(crate) async fn remove_allows(&self, domains: Vec<String>) -> Result<(), Error> { pub(crate) async fn remove_allows(&self, domains: Vec<String>) -> Result<(), Error> {
self.unblock(move |inner| { self.unblock(move |inner| {
if inner.restricted_mode { if inner.restricted_mode {
let connected_by_domain = inner.connected_by_domain(&domains).collect::<Vec<_>>();
let mut connected_batch = Batch::default(); let mut connected_batch = Batch::default();
for connected in &connected_by_domain { for connected in inner.connected_by_domain(&domains) {
connected_batch.remove(connected.as_str().as_bytes()); connected_batch.remove(connected.as_str().as_bytes());
} }

View File

@ -123,9 +123,6 @@ pub(crate) enum ErrorKind {
#[error("Couldn't sign request")] #[error("Couldn't sign request")]
SignRequest, SignRequest,
#[error("Response body from server exceeded limits")]
BodyTooLarge,
#[error("Couldn't make request")] #[error("Couldn't make request")]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),

View File

@ -163,10 +163,6 @@ impl XApiToken {
pub(crate) fn new(token: String) -> Self { pub(crate) fn new(token: String) -> Self {
Self(token) Self(token)
} }
pub(crate) const fn http1_name() -> reqwest::header::HeaderName {
reqwest::header::HeaderName::from_static("x-api-token")
}
} }
impl Header for XApiToken { impl Header for XApiToken {

View File

@ -1,18 +0,0 @@
pub(crate) fn name_to_http02(
name: &reqwest::header::HeaderName,
) -> actix_web::http::header::HeaderName {
actix_web::http::header::HeaderName::from_bytes(name.as_ref())
.expect("headername conversions always work")
}
pub(crate) fn value_to_http02(
value: &reqwest::header::HeaderValue,
) -> actix_web::http::header::HeaderValue {
actix_web::http::header::HeaderValue::from_bytes(value.as_bytes())
.expect("headervalue conversions always work")
}
pub(crate) fn status_to_http02(status: reqwest::StatusCode) -> actix_web::http::StatusCode {
actix_web::http::StatusCode::from_u16(status.as_u16())
.expect("statuscode conversions always work")
}

View File

@ -156,7 +156,7 @@ struct Link {
#[serde(untagged)] #[serde(untagged)]
enum MaybeSupported<T> { enum MaybeSupported<T> {
Supported(T), Supported(T),
Unsupported(#[allow(unused)] String), Unsupported(String),
} }
impl<T> MaybeSupported<T> { impl<T> MaybeSupported<T> {
@ -165,8 +165,8 @@ impl<T> MaybeSupported<T> {
} }
} }
struct SupportedVersion(#[allow(unused)] String); struct SupportedVersion(String);
struct SupportedNodeinfo(#[allow(unused)] String); struct SupportedNodeinfo(String);
static SUPPORTED_VERSIONS: &str = "2."; static SUPPORTED_VERSIONS: &str = "2.";
static SUPPORTED_NODEINFO: &str = "http://nodeinfo.diaspora.software/ns/schema/2."; static SUPPORTED_NODEINFO: &str = "http://nodeinfo.diaspora.software/ns/schema/2.";

View File

@ -12,7 +12,7 @@ use error::Error;
use http_signature_normalization_actix::middleware::VerifySignature; use http_signature_normalization_actix::middleware::VerifySignature;
use metrics_exporter_prometheus::PrometheusBuilder; use metrics_exporter_prometheus::PrometheusBuilder;
use metrics_util::layers::FanoutBuilder; use metrics_util::layers::FanoutBuilder;
use opentelemetry::{trace::TracerProvider, KeyValue}; use opentelemetry::KeyValue;
use opentelemetry_otlp::WithExportConfig; use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource; use opentelemetry_sdk::Resource;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
@ -33,13 +33,11 @@ mod db;
mod error; mod error;
mod extractors; mod extractors;
mod future; mod future;
mod http1;
mod jobs; mod jobs;
mod middleware; mod middleware;
mod requests; mod requests;
mod routes; mod routes;
mod spawner; mod spawner;
mod stream;
mod telegram; mod telegram;
use crate::config::UrlKind; use crate::config::UrlKind;
@ -83,21 +81,22 @@ fn init_subscriber(
let subscriber = subscriber.with(console_layer); let subscriber = subscriber.with(console_layer);
if let Some(url) = opentelemetry_url { if let Some(url) = opentelemetry_url {
let exporter = opentelemetry_otlp::SpanExporter::builder() let tracer = opentelemetry_otlp::new_pipeline()
.with_tonic() .tracing()
.with_endpoint(url.as_str()) .with_trace_config(
.build()?; opentelemetry_sdk::trace::config().with_resource(Resource::new(vec![
KeyValue::new("service.name", software_name),
let tracer_provider = opentelemetry_sdk::trace::TracerProvider::builder() ])),
.with_resource(Resource::new(vec![KeyValue::new( )
"service.name", .with_exporter(
software_name, opentelemetry_otlp::new_exporter()
)])) .tonic()
.with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio) .with_endpoint(url.as_str()),
.build(); )
.install_batch(opentelemetry_sdk::runtime::Tokio)?;
let otel_layer = tracing_opentelemetry::layer() let otel_layer = tracing_opentelemetry::layer()
.with_tracer(tracer_provider.tracer(software_name)) .with_tracer(tracer)
.with_filter(targets); .with_filter(targets);
let subscriber = subscriber.with(otel_layer); let subscriber = subscriber.with(otel_layer);
@ -418,7 +417,7 @@ async fn server_main(
.with_no_client_auth() .with_no_client_auth()
.with_cert_resolver(cert_rx); .with_cert_resolver(cert_rx);
server server
.bind_rustls_0_23(bind_address, server_config)? .bind_rustls_0_22(bind_address, server_config)?
.run() .run()
.await?; .await?;

View File

@ -80,7 +80,7 @@ where
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let log_on_drop = LogOnDrop { let log_on_drop = LogOnDrop {
begin: Instant::now(), begin: Instant::now(),
path: format!("{:?}", req.match_pattern()), path: req.path().to_string(),
method: req.method().to_string(), method: req.method().to_string(),
arm: false, arm: false,
}; };

View File

@ -2,7 +2,6 @@ use crate::{
data::LastOnline, data::LastOnline,
error::{Error, ErrorKind}, error::{Error, ErrorKind},
spawner::Spawner, spawner::Spawner,
stream::{aggregate, limit_stream},
}; };
use activitystreams::iri_string::types::IriString; use activitystreams::iri_string::types::IriString;
use actix_web::http::header::Date; use actix_web::http::header::Date;
@ -25,9 +24,6 @@ const ONE_MINUTE: u64 = 60 * ONE_SECOND;
const ONE_HOUR: u64 = 60 * ONE_MINUTE; const ONE_HOUR: u64 = 60 * ONE_MINUTE;
const ONE_DAY: u64 = 24 * ONE_HOUR; const ONE_DAY: u64 = 24 * ONE_HOUR;
// 20 KB
const JSON_SIZE_LIMIT: usize = 20 * 1024;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum BreakerStrategy { pub(crate) enum BreakerStrategy {
// Requires a successful response // Requires a successful response
@ -233,11 +229,7 @@ impl Requests {
} }
} }
return Err(ErrorKind::Status( return Err(ErrorKind::Status(parsed_url.to_string(), status).into());
parsed_url.to_string(),
crate::http1::status_to_http02(status),
)
.into());
} }
// only actually succeed a breaker on 2xx response // only actually succeed a breaker on 2xx response
@ -270,7 +262,7 @@ impl Requests {
where where
T: serde::de::DeserializeOwned, T: serde::de::DeserializeOwned,
{ {
let stream = self let body = self
.do_deliver( .do_deliver(
url, url,
&serde_json::json!({}), &serde_json::json!({}),
@ -279,9 +271,8 @@ impl Requests {
strategy, strategy,
) )
.await? .await?
.bytes_stream(); .bytes()
.await?;
let body = aggregate(limit_stream(stream, JSON_SIZE_LIMIT)).await?;
Ok(serde_json::from_slice(&body)?) Ok(serde_json::from_slice(&body)?)
} }
@ -308,12 +299,11 @@ impl Requests {
where where
T: serde::de::DeserializeOwned, T: serde::de::DeserializeOwned,
{ {
let stream = self let body = self
.do_fetch_response(url, accept, strategy) .do_fetch_response(url, accept, strategy)
.await? .await?
.bytes_stream(); .bytes()
.await?;
let body = aggregate(limit_stream(stream, JSON_SIZE_LIMIT)).await?;
Ok(serde_json::from_slice(&body)?) Ok(serde_json::from_slice(&body)?)
} }

View File

@ -2,14 +2,10 @@ use crate::{
data::MediaCache, data::MediaCache,
error::Error, error::Error,
requests::{BreakerStrategy, Requests}, requests::{BreakerStrategy, Requests},
stream::limit_stream,
}; };
use actix_web::{body::BodyStream, web, HttpResponse}; use actix_web::{body::BodyStream, web, HttpResponse};
use uuid::Uuid; use uuid::Uuid;
// 16 MB
const IMAGE_SIZE_LIMIT: usize = 16 * 1024 * 1024;
#[tracing::instrument(name = "Media", skip(media, requests))] #[tracing::instrument(name = "Media", skip(media, requests))]
pub(crate) async fn route( pub(crate) async fn route(
media: web::Data<MediaCache>, media: web::Data<MediaCache>,
@ -23,19 +19,13 @@ pub(crate) async fn route(
.fetch_response(&url, BreakerStrategy::Allow404AndBelow) .fetch_response(&url, BreakerStrategy::Allow404AndBelow)
.await?; .await?;
let mut response = HttpResponse::build(crate::http1::status_to_http02(res.status())); let mut response = HttpResponse::build(res.status());
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") { for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
response.insert_header(( response.insert_header((name.clone(), value.clone()));
crate::http1::name_to_http02(name),
crate::http1::value_to_http02(value),
));
} }
return Ok(response.body(BodyStream::new(limit_stream( return Ok(response.body(BodyStream::new(res.bytes_stream())));
res.bytes_stream(),
IMAGE_SIZE_LIMIT,
))));
} }
Ok(HttpResponse::NotFound().finish()) Ok(HttpResponse::NotFound().finish())

View File

@ -1,59 +0,0 @@
use crate::error::{Error, ErrorKind};
use actix_web::web::{Bytes, BytesMut};
use futures_core::Stream;
use streem::IntoStreamer;
pub(crate) fn limit_stream<'a, S>(
input: S,
limit: usize,
) -> impl Stream<Item = Result<Bytes, Error>> + Send + 'a
where
S: Stream<Item = reqwest::Result<Bytes>> + Send + 'a,
{
streem::try_from_fn(move |yielder| async move {
let stream = std::pin::pin!(input);
let mut stream = stream.into_streamer();
let mut count = 0;
while let Some(bytes) = stream.try_next().await? {
count += bytes.len();
if count > limit {
return Err(ErrorKind::BodyTooLarge.into());
}
yielder.yield_ok(bytes).await;
}
Ok(())
})
}
pub(crate) async fn aggregate<S>(input: S) -> Result<Bytes, Error>
where
S: Stream<Item = Result<Bytes, Error>>,
{
let stream = std::pin::pin!(input);
let mut streamer = stream.into_streamer();
let mut buf = Vec::new();
while let Some(bytes) = streamer.try_next().await? {
buf.push(bytes);
}
if buf.len() == 1 {
return Ok(buf.pop().expect("buf has exactly one element"));
}
let total_size: usize = buf.iter().map(|b| b.len()).sum();
let mut bytes_mut = BytesMut::with_capacity(total_size);
for bytes in &buf {
bytes_mut.extend_from_slice(&bytes);
}
Ok(bytes_mut.freeze())
}

View File

@ -75,8 +75,7 @@ pub(crate) fn start(admin_handle: String, db: Db, token: &str) {
fn is_admin(admin_handle: &str, message: &Message) -> bool { fn is_admin(admin_handle: &str, message: &Message) -> bool {
message message
.from .from()
.as_ref()
.and_then(|user| user.username.as_deref()) .and_then(|user| user.username.as_deref())
.map(|username| username == admin_handle) .map(|username| username == admin_handle)
.unwrap_or(false) .unwrap_or(false)