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 = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@ -718,17 +717,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 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]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.31"

View file

@ -16,16 +16,19 @@ lto = true
strip = true strip = true
opt-level = "z" opt-level = "z"
codegen-units = 1 codegen-units = 1
panic = "abort"
[profile.release-local] [profile.release-local]
inherits = "release" inherits = "release"
opt-level = 3 opt-level = 3
strip = false strip = false
panic = "unwind"
[features] [features]
default = [] default = []
env-local = ["axum/tokio", "axum/http1", "axum/http2", "reqwest", "tokio", "env_logger", "governor", "clap", "toml", "image/rayon"] 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"] apparmor = ["dep:rand_core", "dep:siphasher"]
reqwest = ["dep:reqwest"] reqwest = ["dep:reqwest"]
svg-text = ["resvg/text"] svg-text = ["resvg/text"]
@ -37,10 +40,10 @@ governor = ["dep:governor"]
worker = { version="0.4.2", features=['http', 'axum'], optional = true } worker = { version="0.4.2", features=['http', 'axum'], optional = true }
worker-macros = { version="0.4.2", features=['http'], optional = true } worker-macros = { version="0.4.2", features=['http'], optional = true }
axum = { version = "0.7", default-features = false, features = ["query", "json"] } 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 } console_error_panic_hook = { version = "0.1.1", optional = true }
serde = { version = "1.0.214", features = ["derive"] } serde = { version = "1", features = ["derive"] }
futures = "0.3.31" 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"] } 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 } reqwest = { version = "0.12.9", features = ["brotli", "gzip", "stream", "zstd"], optional = true }
rand_core = { version = "0.6.4", features = ["getrandom"], 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 } tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros"], optional = true }
clap = { version = "4.5.20", features = ["derive"], optional = true } clap = { version = "4.5.20", features = ["derive"], optional = true }
toml = { version = "0.8", optional = true } toml = { version = "0.8", optional = true }
log = "0.4.22" log = "0.4"
env_logger = { version = "0.11.5", optional = true } env_logger = { version = "0.11", optional = true }
governor = { version = "0.7.0", features = ["dashmap"], optional = true } governor = { version = "0.7.0", features = ["dashmap"], optional = true }
resvg = { version = "0.44.0", default-features = false, features = ["gif", "image-webp"] } resvg = { version = "0.44.0", default-features = false, features = ["gif", "image-webp"] }
thiserror = "2.0.3" thiserror = "2.0"
[build-dependencies] [build-dependencies]
chumsky = "0.9.3" 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] HTTPs only mode and X-Forwarded-Proto reflection
- [X] Cache-Control header - [X] Cache-Control header
- [X] Rate-limiting on local deployment (untested) - [X] Rate-limiting on local deployment (untested)
- [ ] Read config from Cloudflare
- [ ] Handle all possible panics
## Demo ## Demo

View file

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

View file

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

View file

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

View file

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