Relax some size-critical dependency versions

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-13 11:24:08 -06:00
parent cf1af773a6
commit 5ffbf3bcf8
No known key found for this signature in database
7 changed files with 81 additions and 62 deletions

12
Cargo.lock generated
View file

@ -695,7 +695,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@ -718,17 +717,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"

View file

@ -16,16 +16,19 @@ lto = true
strip = true
opt-level = "z"
codegen-units = 1
panic = "abort"
[profile.release-local]
inherits = "release"
opt-level = 3
strip = false
panic = "unwind"
[features]
default = []
env-local = ["axum/tokio", "axum/http1", "axum/http2", "reqwest", "tokio", "env_logger", "governor", "clap", "toml", "image/rayon"]
cf-worker = ["dep:worker", "dep:worker-macros", "dep:console_error_panic_hook"]
cf-worker = ["dep:worker", "dep:worker-macros"]
panic-console-error = ["dep:console_error_panic_hook"]
apparmor = ["dep:rand_core", "dep:siphasher"]
reqwest = ["dep:reqwest"]
svg-text = ["resvg/text"]
@ -37,10 +40,10 @@ governor = ["dep:governor"]
worker = { version="0.4.2", features=['http', 'axum'], optional = true }
worker-macros = { version="0.4.2", features=['http'], optional = true }
axum = { version = "0.7", default-features = false, features = ["query", "json"] }
tower-service = "0.3.2"
tower-service = "0.3"
console_error_panic_hook = { version = "0.1.1", optional = true }
serde = { version = "1.0.214", features = ["derive"] }
futures = "0.3.31"
serde = { version = "1", features = ["derive"] }
futures = { version = "0.3.31", default-features = false, features = ["std"] }
image = { version = "0.25.5", default-features = false, features = ["avif", "bmp", "gif", "ico", "jpeg", "png", "webp"] }
reqwest = { version = "0.12.9", features = ["brotli", "gzip", "stream", "zstd"], optional = true }
rand_core = { version = "0.6.4", features = ["getrandom"], optional = true }
@ -48,11 +51,11 @@ siphasher = { version = "1.0.1", optional = true }
tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros"], optional = true }
clap = { version = "4.5.20", features = ["derive"], optional = true }
toml = { version = "0.8", optional = true }
log = "0.4.22"
env_logger = { version = "0.11.5", optional = true }
log = "0.4"
env_logger = { version = "0.11", optional = true }
governor = { version = "0.7.0", features = ["dashmap"], optional = true }
resvg = { version = "0.44.0", default-features = false, features = ["gif", "image-webp"] }
thiserror = "2.0.3"
thiserror = "2.0"
[build-dependencies]
chumsky = "0.9.3"

View file

@ -14,6 +14,8 @@ Work in progress! Currently to do:
- [X] HTTPs only mode and X-Forwarded-Proto reflection
- [X] Cache-Control header
- [X] Rate-limiting on local deployment (untested)
- [ ] Read config from Cloudflare
- [ ] Handle all possible panics
## Demo

View file

@ -4,7 +4,6 @@ use axum::{
extract::FromRequestParts,
http::{request::Parts, HeaderMap},
};
use futures::stream::TryStreamExt;
use std::{borrow::Cow, collections::HashSet, convert::Infallible, pin::Pin};
/// Default maximum number of redirects to follow
@ -18,6 +17,7 @@ pub struct RequestCtx<'a> {
}
const fn http_version_to_via(v: axum::http::Version) -> &'static str {
#[allow(clippy::match_same_arms)]
match v {
axum::http::Version::HTTP_09 => "0.9",
axum::http::Version::HTTP_10 => "1.0",
@ -58,6 +58,7 @@ pub struct IncomingInfo {
impl IncomingInfo {
/// Check if the request is potentially looping
#[must_use]
pub fn looping(&self, self_via: &str) -> bool {
if self.user_agent.is_empty() {
return true;
@ -136,10 +137,11 @@ pub trait UpstreamClient {
pub mod reqwest {
use super::{
http_version_to_via, Cow, ErrorResponse, HTTPResponse, HeaderMap, Pin, RequestCtx,
TryStreamExt, UpstreamClient, MAX_SIZE,
UpstreamClient, MAX_SIZE,
};
use ::reqwest::{redirect::Policy, ClientBuilder, Url};
use axum::body::Bytes;
use futures::TryStreamExt;
use reqwest::dns::Resolve;
use std::{sync::Arc, time::Duration};
@ -362,7 +364,7 @@ pub mod cf_worker {
UpstreamClient, MAX_SIZE,
};
use axum::http::{HeaderName, HeaderValue};
use futures::{FutureExt, Stream, TryFutureExt};
use futures::{Stream, TryFutureExt};
use worker::{
AbortController, ByteStream, CfProperties, Fetch, Headers, Method, PolishConfig, Request,
RequestInit, RequestRedirect, Url,

View file

@ -4,9 +4,9 @@
#![warn(missing_docs)]
#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
use std::{
borrow::Cow, fmt::Display, marker::PhantomData, net::SocketAddr, num::NonZero, sync::Arc,
};
use std::{borrow::Cow, fmt::Display, marker::PhantomData, sync::Arc};
#[cfg(feature = "governor")]
use std::{net::SocketAddr, num::NonZero};
#[cfg(feature = "governor")]
use axum::extract::ConnectInfo;
@ -185,13 +185,13 @@ pub fn router<C: UpstreamClient + 'static, S: Sandboxing + 'static>(config: Conf
where
<<C as UpstreamClient>::Response as HTTPResponse>::BodyStream: Unpin,
{
use std::time::Duration;
use axum::middleware;
#[cfg(feature = "governor")]
use governor::{
clock::SystemClock, middleware::StateInformationMiddleware, Quota, RateLimiter,
};
#[cfg(feature = "governor")]
use std::time::Duration;
let state = AppState {
#[cfg(feature = "governor")]
@ -199,7 +199,7 @@ where
Quota::with_period(Duration::from_millis(config.rate_limit.replenish_every))
.unwrap()
.allow_burst(config.rate_limit.burst),
SystemClock::default(),
SystemClock,
)
.with_middleware::<StateInformationMiddleware>(),
client: Upstream::new(&config.fetch),
@ -328,7 +328,7 @@ pub async fn rate_limit_middleware(
headers.insert("X-RateLimit-Remaining", "0".parse().unwrap());
headers.insert(
"Retry-After",
err.wait_time_from(SystemTime::now().into())
err.wait_time_from(SystemTime::now())
.as_secs()
.to_string()
.parse()
@ -350,9 +350,9 @@ async fn fetch(
use fetch::cf_worker::CfWorkerClient;
use tower_service::Service;
#[cfg(all(feature = "cf-worker", target_arch = "wasm32"))]
#[cfg(feature = "panic-console-error")]
console_error_panic_hook::set_once();
Ok(router::<CfWorkerClient, NoSandbox>(Default::default())
Ok(router::<CfWorkerClient, NoSandbox>(Config::default())
.call(req)
.await?)
}
@ -404,6 +404,7 @@ pub struct ImageOptions {
impl ImageOptions {
/// Whether post-processing is requested
#[must_use]
pub fn requested_postprocess(&self) -> bool {
self.format.is_some()
|| self.avatar.is_some()
@ -467,6 +468,7 @@ impl std::error::Error for ErrorResponse {}
impl ErrorResponse {
/// Method not allowed
#[must_use]
pub const fn method_not_allowed() -> Self {
Self {
status: StatusCode::METHOD_NOT_ALLOWED,
@ -486,7 +488,7 @@ impl ErrorResponse {
pub fn unexpected_status(url: &str, status: u16) -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
message: format!("Unexpected status code when accessing {}: {}", url, status).into(),
message: format!("Unexpected status code when accessing {url}: {status}").into(),
}
}
/// Insecure request
@ -676,7 +678,7 @@ impl<C: UpstreamClient + 'static, S: Sandboxing + 'static> App<C, S> {
{
let mut options = query.image_options;
if let Some(filename) = filename {
options.apply_filename(&filename);
options.apply_filename(filename);
}
match method {
http::Method::GET => {}
@ -684,9 +686,10 @@ impl<C: UpstreamClient + 'static, S: Sandboxing + 'static> App<C, S> {
let mut resp = Response::new(Body::empty());
resp.headers_mut().insert(
"Content-Type",
#[allow(clippy::match_same_arms)]
match options.format.as_deref() {
Some("png") => "image/png",
Some("jpeg") | Some("jpg") => "image/jpeg",
Some("jpeg" | "jpg") => "image/jpeg",
Some("webp") => "image/webp",
_ => "image/webp",
}
@ -705,8 +708,11 @@ impl<C: UpstreamClient + 'static, S: Sandboxing + 'static> App<C, S> {
.request_upstream(&info, &query.url, false, true, DEFAULT_MAX_REDIRECTS)
.await?;
let media =
MediaResponse::from_upstream_response(resp, &state.config.post_process, options)
let media = Box::pin(MediaResponse::from_upstream_response(
resp,
&state.config.post_process,
options,
))
.await?;
Ok(media.into_response())

View file

@ -7,7 +7,7 @@ use image::{
use crate::ImageOptions;
pub const fn clamp_width(input: (u32, u32), max_width: u32) -> (u32, u32) {
const fn clamp_width(input: (u32, u32), max_width: u32) -> (u32, u32) {
if input.0 > max_width {
(max_width, input.1 * max_width / input.0)
} else {
@ -15,7 +15,7 @@ pub const fn clamp_width(input: (u32, u32), max_width: u32) -> (u32, u32) {
}
}
pub const fn clamp_height(input: (u32, u32), max_height: u32) -> (u32, u32) {
const fn clamp_height(input: (u32, u32), max_height: u32) -> (u32, u32) {
if input.1 > max_height {
(input.0 * max_height / input.1, max_height)
} else {
@ -23,12 +23,13 @@ pub const fn clamp_height(input: (u32, u32), max_height: u32) -> (u32, u32) {
}
}
pub const fn clamp_dimensions(input: (u32, u32), max_width: u32, max_height: u32) -> (u32, u32) {
const fn clamp_dimensions(input: (u32, u32), max_width: u32, max_height: u32) -> (u32, u32) {
clamp_height(clamp_width(input, max_width), max_height)
}
// All constants are following https://github.com/misskey-dev/media-proxy/blob/master/SPECIFICATION.md
/// Postprocesses an WebP image using the given options
pub fn postprocess_webp_image(
data: &[u8],
opt: &ImageOptions,
@ -36,7 +37,7 @@ pub fn postprocess_webp_image(
let dec = WebPDecoder::new(Cursor::new(data))?;
if !dec.has_animation() {
return Ok(Some(postprocess_static_image(data, &opt)?));
return Ok(Some(postprocess_static_image(data, opt)?));
}
if opt.static_ == Some(true) {
@ -53,11 +54,12 @@ pub fn postprocess_webp_image(
Ok(None)
}
/// Postprocesses an PNG image using the given options
pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Option<DynamicImage>> {
let dec = PngDecoder::new(Cursor::new(data))?;
if dec.is_apng()? {
return Ok(Some(postprocess_static_image(data, &opt)?));
return Ok(Some(postprocess_static_image(data, opt)?));
}
if opt.static_ == Some(true) {
@ -75,13 +77,17 @@ pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Opt
}
#[derive(Debug, thiserror::Error)]
/// An error that occurred during SVG postprocessing
pub enum SvgPostprocessError {
#[error("Image error: {0}")]
Image(#[from] image::ImageError),
/// An error that occurred during rasterization
#[error("Rasterization error: {0}")]
Rasterization(#[from] image::ImageError),
/// An error that occurred during SVG parsing
#[error("SVG error: {0}")]
Svg(#[from] resvg::usvg::Error),
}
/// Preprocesses an SVG image using the given options. By specs SVG must be rasterized as opposed to passing the SVG data directly for security reasons.
pub fn postprocess_svg_image(
data: &[u8],
opt: &ImageOptions,
@ -103,8 +109,8 @@ pub fn postprocess_svg_image(
let clamped = clamp_dimensions((size.width() as u32, size.height() as u32), 800, 800);
let transform = Transform::from_scale(
clamped.0 as f32 / size.width() as f32,
clamped.1 as f32 / size.height() as f32,
clamped.0 as f32 / size.width(),
clamped.1 as f32 / size.height(),
);
let mut pm = Pixmap::new(clamped.0 as _, clamped.1 as _).unwrap();
@ -116,14 +122,15 @@ pub fn postprocess_svg_image(
Ok(process_static_image_impl(
DynamicImage::ImageRgba8(image),
&opt,
opt,
))
}
/// Preprocesses a static image using the given options
pub fn postprocess_static_image(data: &[u8], opt: &ImageOptions) -> ImageResult<DynamicImage> {
Ok(process_static_image_impl(
image::load_from_memory(data)?,
&opt,
opt,
))
}

View file

@ -68,24 +68,22 @@ where
// svg need special handling so we deal with it first
let is_svg = claimed_ct
.as_deref()
.map(|ct| ct.starts_with("image/svg"))
.unwrap_or(false);
.is_some_and(|ct| ct.starts_with("image/svg"));
// first if the media type is not something we can handle
if !is_svg
&& (!options.requested_postprocess()
|| claimed_ct
.map(|ct| ct.starts_with("video/") || ct.starts_with("audio/"))
.unwrap_or(false))
.is_some_and(|ct| ct.starts_with("video/") || ct.starts_with("audio/")))
{
if config.enable_redirects
&& options.origin != Some(true)
&& content_length.map_or(false, |cl| cl > 1 << 20)
{
return Ok(MediaResponse::Redirect(response.request().url.to_string()));
} else {
return Ok(MediaResponse::probe_then_through(response).await?);
}
return MediaResponse::probe_then_through(response).await;
}
let is_https = response.request().secure;
@ -152,10 +150,17 @@ where
let header = header.into_inner();
let mut buf = if let Some(cl) = content_length {
let mut ret = Vec::with_capacity(cl);
ret.extend_from_slice(&header[..header_len as usize]);
ret.extend_from_slice(
&header[..header_len
.try_into()
.map_err(|_| ErrorResponse::payload_too_large())?],
);
ret
} else {
header[..header_len as usize].to_vec()
header[..header_len
.try_into()
.map_err(|_| ErrorResponse::payload_too_large())?]
.to_vec()
};
while let Some(Ok(bytes)) = remaining_body.next().await {
if buf.len() + bytes.as_ref().len() > SLURP_LIMIT {
@ -241,8 +246,11 @@ where
}))
} else {
Ok(MediaResponse::PassThru(PassThru {
header_len: header.position() as _,
header: header.into_inner(),
header_len: header
.position()
.try_into()
.map_err(|_| ErrorResponse::payload_too_large())?,
header: Box::new(header.into_inner()),
remaining_body,
content_type: Some(mime.to_string()),
is_https,
@ -250,8 +258,11 @@ where
}
}
None => Ok(MediaResponse::PassThru(PassThru {
header_len: header.position() as _,
header: header.into_inner(),
header_len: header
.position()
.try_into()
.map_err(|_| ErrorResponse::payload_too_large())?,
header: Box::new(header.into_inner()),
remaining_body,
content_type: None,
is_https,
@ -335,7 +346,7 @@ where
.position()
.try_into()
.map_err(|_| ErrorResponse::payload_too_large())?,
header: header.into_inner(),
header: Box::new(header.into_inner()),
remaining_body: body,
content_type,
is_https,
@ -348,7 +359,7 @@ where
/// Pass through the response
pub struct PassThru<R: HTTPResponse> {
header: [u8; MTU_BUFFER_SIZE],
header: Box<[u8; MTU_BUFFER_SIZE]>,
header_len: usize,
content_type: Option<String>,
is_https: bool,