even more defensive programming on image handling
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
30a0e09a22
commit
607eaae3a9
5 changed files with 122 additions and 13 deletions
16
README.md
16
README.md
|
@ -13,12 +13,12 @@ Work in progress! Currently to do:
|
||||||
- [X] RFC9110 compliant proxy loop detection with defensive programming against known vulnerable proxies
|
- [X] RFC9110 compliant proxy loop detection with defensive programming against known vulnerable proxies
|
||||||
- [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
|
||||||
- [X] Read config from Cloudflare
|
- [X] Read config from Cloudflare
|
||||||
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
|
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
|
||||||
- [X] Tiered rate-limiting
|
- [X] Tiered rate-limiting
|
||||||
- [ ] Lossy WebP on CF Workers (maybe already works?)
|
- [ ] Lossy WebP on CF Workers (maybe already works?)
|
||||||
- [ ] Cache Results on Cloudflare KV.
|
- [ ] Cache results
|
||||||
- [ ] Handle all possible panics reported by Clippy
|
- [ ] Handle all possible panics reported by Clippy
|
||||||
- [X] Sandboxing the image rendering
|
- [X] Sandboxing the image rendering
|
||||||
- [X] Prometheus-format metrics
|
- [X] Prometheus-format metrics
|
||||||
|
@ -29,21 +29,29 @@ Work in progress! Currently to do:
|
||||||
|
|
||||||
Preview at:
|
Preview at:
|
||||||
|
|
||||||
[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||||
|
|
||||||
|
Local: [https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||||
|
|
||||||
Image:
|
Image:
|
||||||
|
|
||||||
![Syuilo Avatar resived.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
![Syuilo Avatar resived.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||||
|
|
||||||
|
![Syuilo Avatar resived.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||||
|
|
||||||
|
|
||||||
### SVG rendering
|
### SVG rendering
|
||||||
|
|
||||||
(font rendering disabled due to size restrictions)
|
(font rendering disabled due to size restrictions)
|
||||||
|
|
||||||
[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||||
|
|
||||||
|
Local: [https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||||
|
|
||||||
![AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
![AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||||
|
|
||||||
|
![AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||||
|
|
||||||
## Setup and Deployment
|
## Setup and Deployment
|
||||||
|
|
||||||
1. Clone this repository. Load the submodules with `git submodule update --init`.
|
1. Clone this repository. Load the submodules with `git submodule update --init`.
|
||||||
|
|
23
src/lib.rs
23
src/lib.rs
|
@ -813,6 +813,29 @@ pub struct App<C: UpstreamClient, S: Sandboxing> {
|
||||||
_marker: PhantomData<(C, S)>,
|
_marker: PhantomData<(C, S)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "env-local")]
|
||||||
|
/// Register cancel handler for threads that has ran away for some reason
|
||||||
|
///
|
||||||
|
/// This should only happen when some kind of malformed input is happening that causes the image parser to hang
|
||||||
|
/// without triggering the resource limits.
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
pub fn register_cancel_handler() {
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
unsafe {
|
||||||
|
unsafe fn cancel_handler(_: libc::c_int) {
|
||||||
|
log::error!("Received cancel signal, stopping thread");
|
||||||
|
libc::pthread_exit(&sandbox::EXIT_TIMEOUT as *const _ as *mut _);
|
||||||
|
}
|
||||||
|
|
||||||
|
if libc::signal(libc::SIGUSR1, cancel_handler as usize) == libc::SIG_ERR {
|
||||||
|
log::error!(
|
||||||
|
"Failed to register cancel handler: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "cf-worker", feature = "reqwest"))]
|
#[cfg(any(feature = "cf-worker", feature = "reqwest"))]
|
||||||
#[allow(clippy::unused_async)]
|
#[allow(clippy::unused_async)]
|
||||||
impl<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static> App<C, S> {
|
impl<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static> App<C, S> {
|
||||||
|
|
|
@ -225,6 +225,8 @@ fn main() {
|
||||||
|
|
||||||
log::info!("Spawned Tokio reactor.");
|
log::info!("Spawned Tokio reactor.");
|
||||||
|
|
||||||
|
yumechi_no_kuni_proxy_worker::register_cancel_handler();
|
||||||
|
|
||||||
runtime
|
runtime
|
||||||
.block_on(async move {
|
.block_on(async move {
|
||||||
#[cfg(not(feature = "axum-server"))]
|
#[cfg(not(feature = "axum-server"))]
|
||||||
|
|
|
@ -2,7 +2,8 @@ use std::io::Cursor;
|
||||||
|
|
||||||
use image::{
|
use image::{
|
||||||
codecs::{png::PngDecoder, webp::WebPDecoder},
|
codecs::{png::PngDecoder, webp::WebPDecoder},
|
||||||
AnimationDecoder, DynamicImage, GenericImageView, ImageResult,
|
AnimationDecoder, DynamicImage, GenericImageView, ImageDecoder, ImageReader, ImageResult,
|
||||||
|
Limits,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ImageOptions;
|
use crate::ImageOptions;
|
||||||
|
@ -34,7 +35,9 @@ pub fn postprocess_webp_image(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
opt: &ImageOptions,
|
opt: &ImageOptions,
|
||||||
) -> ImageResult<Option<DynamicImage>> {
|
) -> ImageResult<Option<DynamicImage>> {
|
||||||
let dec = WebPDecoder::new(Cursor::new(data))?;
|
let mut dec = WebPDecoder::new(Cursor::new(data))?;
|
||||||
|
dec.set_limits(default_limits())
|
||||||
|
.expect("Failed to set limits");
|
||||||
|
|
||||||
if !dec.has_animation() && opt.requested_resize() {
|
if !dec.has_animation() && opt.requested_resize() {
|
||||||
return Ok(Some(postprocess_static_image(data, opt)?));
|
return Ok(Some(postprocess_static_image(data, opt)?));
|
||||||
|
@ -56,7 +59,9 @@ pub fn postprocess_webp_image(
|
||||||
|
|
||||||
/// Postprocesses an PNG image using the given options
|
/// 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 mut dec = PngDecoder::new(Cursor::new(data))?;
|
||||||
|
dec.set_limits(default_limits())
|
||||||
|
.expect("Failed to set limits");
|
||||||
|
|
||||||
let apng = dec.is_apng()?;
|
let apng = dec.is_apng()?;
|
||||||
|
|
||||||
|
@ -81,6 +86,9 @@ 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
|
/// An error that occurred during SVG postprocessing
|
||||||
pub enum SvgPostprocessError {
|
pub enum SvgPostprocessError {
|
||||||
|
/// The complexity of the SVG image exceeded the limit
|
||||||
|
#[error("SVG complexity limit exceeded: computed complexity is {0}")]
|
||||||
|
LimitExceeded(usize),
|
||||||
/// An error that occurred during rasterization
|
/// An error that occurred during rasterization
|
||||||
#[error("Rasterization error: {0}")]
|
#[error("Rasterization error: {0}")]
|
||||||
Rasterization(#[from] image::ImageError),
|
Rasterization(#[from] image::ImageError),
|
||||||
|
@ -123,6 +131,17 @@ pub fn postprocess_svg_image(
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let complexity = svg.clip_paths().len()
|
||||||
|
+ svg.filters().len()
|
||||||
|
+ svg.linear_gradients().len()
|
||||||
|
+ svg.radial_gradients().len()
|
||||||
|
+ svg.patterns().len()
|
||||||
|
+ (svg.size().width() * svg.size().height()) as usize / 1000;
|
||||||
|
|
||||||
|
if complexity > 25535 {
|
||||||
|
return Err(SvgPostprocessError::LimitExceeded(complexity));
|
||||||
|
}
|
||||||
|
|
||||||
let size = svg.size();
|
let size = svg.size();
|
||||||
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);
|
||||||
|
|
||||||
|
@ -144,8 +163,21 @@ pub fn postprocess_svg_image(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_limits() -> Limits {
|
||||||
|
let mut l = Limits::default();
|
||||||
|
l.max_alloc = Some(64 << 20);
|
||||||
|
l.max_image_height = Some(8192);
|
||||||
|
l.max_image_width = Some(8192);
|
||||||
|
|
||||||
|
l
|
||||||
|
}
|
||||||
|
|
||||||
/// Preprocesses a static image using the given options
|
/// 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> {
|
||||||
|
let mut r = ImageReader::new(Cursor::new(data));
|
||||||
|
|
||||||
|
r.limits(default_limits());
|
||||||
|
|
||||||
Ok(process_static_image_impl(
|
Ok(process_static_image_impl(
|
||||||
image::load_from_memory(data)?,
|
image::load_from_memory(data)?,
|
||||||
opt,
|
opt,
|
||||||
|
|
|
@ -1,7 +1,49 @@
|
||||||
use crate::SandboxConfig;
|
use crate::SandboxConfig;
|
||||||
|
|
||||||
#[cfg(all(target_family = "unix", feature = "tokio"))]
|
/// Thread termination due to timeout
|
||||||
|
pub const EXIT_TIMEOUT: usize = 1;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub(crate) mod pthread {
|
||||||
|
|
||||||
|
#[link(name = "pthread")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn pthread_setcancelstate(state: i32, oldstate: *mut i32) -> i32;
|
||||||
|
pub fn pthread_setcanceltype(type_: i32, oldtype: *mut i32) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PTHREAD_CANCEL_ENABLE: i32 = 0;
|
||||||
|
pub const PTHREAD_CANCEL_ASYNCHRONOUS: i32 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a function with explicit immediate cancellation
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
|
pub(crate) fn pthread_cancelable<F: FnOnce() -> R, R>(f: F) -> R {
|
||||||
|
unsafe {
|
||||||
|
let mut oldstate = 0;
|
||||||
|
let mut oldtype = 0;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pthread::pthread_setcancelstate(pthread::PTHREAD_CANCEL_ENABLE, &mut oldstate);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pthread::pthread_setcanceltype(pthread::PTHREAD_CANCEL_ASYNCHRONOUS, &mut oldtype);
|
||||||
|
log::trace!(
|
||||||
|
"Running cancelable function, oldstate: {}, oldtype: {}",
|
||||||
|
oldstate,
|
||||||
|
oldtype
|
||||||
|
);
|
||||||
|
let res = f();
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pthread::pthread_setcancelstate(oldstate, std::ptr::null_mut());
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pthread::pthread_setcanceltype(oldtype, std::ptr::null_mut());
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_family = "unix", feature = "tokio"))]
|
||||||
|
#[allow(unsafe_code, clippy::inline_always)]
|
||||||
|
#[inline(always)]
|
||||||
/// A bloking tokio task with a cancellation
|
/// A bloking tokio task with a cancellation
|
||||||
pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
|
pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
|
||||||
sandbox: &impl Sandboxing,
|
sandbox: &impl Sandboxing,
|
||||||
|
@ -14,18 +56,20 @@ pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if signal.await.is_ok() {
|
if signal.await.is_ok() {
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::pthread_cancel(tid);
|
libc::pthread_kill(tid, libc::SIGUSR1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
|
pthread_cancelable(|| {
|
||||||
let guard = sandbox.setup(key);
|
let guard = sandbox.setup(key);
|
||||||
let res = f();
|
let res = f();
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
res
|
res
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for setting up a thread sandboxing environment
|
/// A trait for setting up a thread sandboxing environment
|
||||||
|
|
Loading…
Reference in a new issue