Relax some size-critical dependency versions
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
cf1af773a6
commit
5ffbf3bcf8
7 changed files with 81 additions and 62 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -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"
|
||||
|
|
17
Cargo.toml
17
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
36
src/lib.rs
36
src/lib.rs
|
@ -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,9 +708,12 @@ 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)
|
||||
.await?;
|
||||
let media = Box::pin(MediaResponse::from_upstream_response(
|
||||
resp,
|
||||
&state.config.post_process,
|
||||
options,
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(media.into_response())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue