yumechi-no-kuni-proxy-worker/src/lib.rs
eternal-flame-AD acd707e0d7
increase initial accepted content-length
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-20 06:18:35 -06:00

938 lines
30 KiB
Rust

#![doc = include_str!("../README.md")]
#![warn(clippy::all, clippy::pedantic)]
#![warn(unsafe_code)]
#![warn(missing_docs)]
#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
#[cfg(feature = "governor")]
use std::net::SocketAddr;
use std::{
borrow::Cow,
fmt::Display,
marker::PhantomData,
sync::{atomic::AtomicU64, Arc, RwLock},
};
#[cfg(feature = "governor")]
use axum::extract::ConnectInfo;
use axum::{
body::Body,
extract::{Path, Query, State},
http::{self, HeaderMap, StatusCode},
response::{IntoResponse, Redirect, Response},
routing::get,
Json, Router,
};
use fetch::{HTTPResponse, IncomingInfo, UpstreamClient, DEFAULT_MAX_REDIRECTS};
#[cfg(feature = "governor")]
use governor::{
clock::SystemClock, middleware::StateInformationMiddleware, state::keyed::DashMapStateStore,
RateLimiter,
};
use lru::LruCache;
use post_process::{CompressionLevel, MediaResponse};
use sandbox::Sandboxing;
use serde::Deserialize;
#[cfg(feature = "cf-worker")]
use worker::{event, Context, Env, HttpRequest, Result as WorkerResult};
use config::{
Config, FetchConfig, IndexConfig, NormalizationPolicy, PostProcessConfig, SandboxConfig,
};
/// Module for fetching media from upstream
pub mod fetch;
/// Module for post-processing media
pub mod post_process;
/// Sandbox utilities for processing media
pub mod sandbox;
/// Stream utilities
pub mod stream;
/// Configuration utilities
pub mod config;
/// Cross platform timing utilities
pub mod timing;
/// Utilities for Cloudflare Workers
#[cfg(feature = "cf-worker")]
mod cf_utils;
const MAX_SIZE: usize = 128 << 20;
#[cfg(all(not(feature = "cf-worker"), not(feature = "reqwest")))]
compile_error!("At least one of the `cf-worker` or `reqwest` features must be enabled. hint: '--features env-local' enables everything related to local runtime");
#[cfg(feature = "cf-worker")]
/// The upstream client chosen by the build configuration
pub type Upstream = crate::fetch::cf_worker::CfWorkerClient;
#[cfg(all(not(feature = "cf-worker"), feature = "reqwest"))]
/// The upstream client chosen by the build configuration
pub type Upstream = crate::fetch::reqwest::ReqwestClient;
#[cfg(feature = "cf-worker")]
#[event(fetch)]
async fn fetch(
req: HttpRequest,
env: Env,
_ctx: Context,
) -> WorkerResult<axum::http::Response<axum::body::Body>> {
use fetch::cf_worker::CfWorkerClient;
use sandbox::NoSandbox;
use tower_service::Service;
log::set_logger(&cf_utils::CfLogger).ok();
let config = match Config::load_from_cf_env(env) {
Ok(config) => config,
Err(e) => {
log::error!("Failed to load configuration: {}", e);
return Ok(ErrorResponse::worker_config_error(e).into_response());
}
};
#[cfg(feature = "panic-console-error")]
console_error_panic_hook::set_once();
Ok(router::<CfWorkerClient, NoSandbox>(config)
.call(req)
.await?)
}
#[cfg(any(feature = "cf-worker", feature = "reqwest"))]
/// Application Router
pub fn router<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static>(
config: Config,
) -> Router
where
<<C as UpstreamClient>::Response as HTTPResponse>::BodyStream: Unpin,
{
use axum::middleware;
#[cfg(feature = "governor")]
use governor::{
clock::SystemClock, middleware::StateInformationMiddleware, Quota, RateLimiter,
};
#[cfg(feature = "governor")]
use std::time::Duration;
use std::{num::NonZero, sync::RwLock};
#[cfg(not(feature = "cf-worker"))]
use tower_http::{catch_panic::CatchPanicLayer, timeout::TimeoutLayer};
let state = AppState {
#[cfg(feature = "governor")]
limiters: config
.rate_limit
.iter()
.map(|x| {
(
RateLimiter::dashmap_with_clock(
Quota::with_period(Duration::from_millis(x.replenish_every))
.unwrap()
.allow_burst(x.burst),
SystemClock,
)
.with_middleware::<StateInformationMiddleware>(),
RwLock::new(LruCache::new(NonZero::new(1024).unwrap())),
)
})
.collect::<Vec<_>>()
.into_boxed_slice(),
client: Upstream::new(&config.fetch),
sandbox: S::new(&config.sandbox),
config,
};
let state = Arc::new(state);
#[allow(unused_mut)]
let mut router = Router::new()
.route("/", get(App::<C, S>::index))
.route(
"/proxy",
get(App::<C, S>::proxy_without_filename)
.head(App::<C, S>::proxy_without_filename)
.options(App::<C, S>::proxy_options)
.route_layer(middleware::from_fn_with_state(
state.clone(),
set_cache_control,
))
.fallback(|| async { ErrorResponse::method_not_allowed() }),
)
.route(
"/proxy/",
get(App::<C, S>::proxy_without_filename)
.head(App::<C, S>::proxy_without_filename)
.options(App::<C, S>::proxy_options)
.route_layer(middleware::from_fn_with_state(
state.clone(),
set_cache_control,
))
.fallback(|| async { ErrorResponse::method_not_allowed() }),
)
.route(
"/proxy/:filename",
get(App::<C, S>::proxy_with_filename)
.head(App::<C, S>::proxy_with_filename)
.options(App::<C, S>::proxy_options)
.route_layer(middleware::from_fn_with_state(
state.clone(),
set_cache_control,
))
.fallback(|| async { ErrorResponse::method_not_allowed() }),
)
.layer(middleware::from_fn(common_security_headers))
.with_state(Arc::clone(&state));
#[cfg(not(feature = "cf-worker"))]
{
router = router
.layer(CatchPanicLayer::custom(|err| {
log::error!("Panic in request: {:?}", err);
ErrorResponse::postprocess_failed("Internal server error".into()).into_response()
}))
.layer(TimeoutLayer::new(Duration::from_secs(10)));
}
#[cfg(feature = "governor")]
{
let state_gc = Arc::clone(&state);
std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(300));
for (limiter, _) in state_gc.limiters.iter() {
limiter.retain_recent();
}
});
}
#[cfg(feature = "governor")]
return router.route_layer(middleware::from_fn_with_state(state, rate_limit_middleware));
#[cfg(not(feature = "governor"))]
router
}
/// Set the Cache-Control header
#[cfg_attr(feature = "cf-worker", worker::send)]
pub async fn set_cache_control<S: Sandboxing + Send + Sync + 'static>(
State(state): State<Arc<AppState<Upstream, S>>>,
request: axum::extract::Request,
next: axum::middleware::Next,
) -> Response {
let mut resp = next.run(request).await;
if state.config.enable_cache {
if resp.status() == StatusCode::OK {
let headers = resp.headers_mut();
headers.insert(
"Cache-Control",
"public, max-age=31536000, immutable".parse().unwrap(),
);
} else {
let headers = resp.headers_mut();
headers.insert("Cache-Control", "max-age=300".parse().unwrap());
}
} else {
let headers = resp.headers_mut();
headers.insert("Cache-Control", "no-store".parse().unwrap());
}
resp
}
/// Middleware for common security headers
pub async fn common_security_headers(
request: axum::extract::Request,
next: axum::middleware::Next,
) -> Response {
let mut resp = next.run(request).await;
let hdr = resp.headers_mut();
if !hdr.contains_key("Content-Security-Policy") {
hdr.insert(
"Content-Security-Policy",
"default-src 'self'".parse().unwrap(),
);
}
hdr.insert("X-Content-Type-Options", "nosniff".parse().unwrap());
hdr.insert("X-Frame-Options", "DENY".parse().unwrap());
hdr.insert("X-XSS-Protection", "1; mode=block".parse().unwrap());
hdr.insert("Permissions-Policy", "interest-cohort=()".parse().unwrap());
resp
}
fn atomic_u64_saturating_dec(credits: &AtomicU64) -> bool {
loop {
let current = credits.load(std::sync::atomic::Ordering::Relaxed);
if current == 0 {
return false;
}
if credits.compare_exchange_weak(
current,
current - 1,
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
) == Ok(current)
{
return true;
}
}
}
/// Middleware for rate limiting
#[cfg(feature = "governor")]
#[cfg_attr(feature = "cf-worker", worker::send)]
pub async fn rate_limit_middleware<S: Sandboxing + Send + Sync + 'static>(
State(state): State<Arc<AppState<Upstream, S>>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
request: axum::extract::Request,
next: axum::middleware::Next,
) -> Response {
use std::{
net::{IpAddr, Ipv6Addr},
time::SystemTime,
};
use axum::http::HeaderName;
let forwarded_ip = if state.config.max_x_forwarded_for > 0 {
std::iter::repeat(addr.ip())
.chain(
request
.headers()
.get("x-forwarded-for")
.and_then(|x| x.to_str().ok())
.unwrap_or("")
.split(',')
.filter_map(|x| x.trim().parse().ok()),
)
.nth_back(state.config.max_x_forwarded_for as usize - 1)
.map(|addr| match addr {
IpAddr::V6(addr) => IpAddr::V6(addr & Ipv6Addr::from_bits(!0u128 >> 64)),
addr => addr,
})
} else {
None
};
let real_ip = forwarded_ip.unwrap_or_else(|| match addr.ip() {
IpAddr::V6(addr) => IpAddr::V6(addr & Ipv6Addr::from_bits(!0u128 >> 64)),
addr => addr,
});
let mut res = Vec::with_capacity(state.limiters.len());
match state.limiters.iter().fold(Ok(()), |acc, limiter| {
acc.and_then(|_| {
// if we have credits, we don't need to check the key
let credits = limiter.1.read().unwrap();
if let Some(credits_val) = credits.peek(&forwarded_ip.unwrap_or(real_ip)) {
if atomic_u64_saturating_dec(credits_val) {
res.push(None);
return Ok(());
}
drop(credits);
let mut credits = limiter.1.write().unwrap();
credits.pop(&forwarded_ip.unwrap_or(real_ip));
}
limiter
.0
.check_key(&forwarded_ip.unwrap_or(real_ip))
.map(|x| res.push(Some(x)))
})
}) {
Ok(_) => {
let begin = SystemTime::now();
let mut resp = next.run(request).await;
let elapsed = begin.elapsed().unwrap().as_millis() as u64;
// credit back limits that didn't get used
for (config, (_, credits)) in state.config.rate_limit.iter().zip(state.limiters.iter())
{
if elapsed < config.min_request_duration.unwrap_or(0) {
let mut credits = credits.write().unwrap();
credits
.get_or_insert(forwarded_ip.unwrap_or(real_ip), || 0.into())
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
let headers = resp.headers_mut();
for (config, snapshot) in
state
.config
.rate_limit
.iter()
.zip(res.iter())
.filter_map(|(config, snapshot)| match snapshot {
Some(snapshot) => Some((config, snapshot)),
None => None,
})
{
let header_prefix = match config.key {
Some(ref key) => format!("X-{}-RateLimit-", key),
None => "X-RateLimit-".to_string(),
};
let (header_limit, header_interval, header_remaining) = (
format!("{}Limit", header_prefix),
format!("{}Replenish-Interval", header_prefix),
format!("{}Remaining", header_prefix),
);
headers.insert(
HeaderName::from_bytes(header_limit.as_bytes()).unwrap(),
#[allow(clippy::unwrap_used)]
snapshot.quota().burst_size().to_string().parse().unwrap(),
);
headers.insert(
HeaderName::from_bytes(header_interval.as_bytes()).unwrap(),
config.replenish_every.to_string().parse().unwrap(),
);
headers.insert("X-RateLimit-Remaining", "0".parse().unwrap());
headers.insert(
HeaderName::from_bytes(header_remaining.as_bytes()).unwrap(),
snapshot
.remaining_burst_capacity()
.to_string()
.parse()
.unwrap(),
);
}
resp
}
Err(err) => {
log::warn!("Rate limit exceeded for {}: {}", addr, err);
let mut resp = ErrorResponse::rate_limit_exceeded().into_response();
let headers = resp.headers_mut();
for (config, snapshot) in
state
.config
.rate_limit
.iter()
.zip(res.iter())
.filter_map(|(config, snapshot)| match snapshot {
Some(snapshot) => Some((config, snapshot)),
None => None,
})
{
let header_prefix = match config.key {
Some(ref key) => format!("X-{}-RateLimit-", key),
None => "X-RateLimit-".to_string(),
};
let (header_limit, header_interval, header_remaining) = (
format!("{}Limit", header_prefix),
format!("{}Replenish-Interval", header_prefix),
format!("{}Remaining", header_prefix),
);
headers.insert(
HeaderName::from_bytes(header_limit.as_bytes()).unwrap(),
#[allow(clippy::unwrap_used)]
snapshot.quota().burst_size().to_string().parse().unwrap(),
);
headers.insert(
HeaderName::from_bytes(header_interval.as_bytes()).unwrap(),
config.replenish_every.to_string().parse().unwrap(),
);
headers.insert("X-RateLimit-Remaining", "0".parse().unwrap());
headers.insert(
HeaderName::from_bytes(header_remaining.as_bytes()).unwrap(),
snapshot
.remaining_burst_capacity()
.to_string()
.parse()
.unwrap(),
);
}
headers.insert(
"Retry-After",
err.earliest_possible()
.duration_since(SystemTime::now())
.unwrap()
.as_secs()
.into(),
);
resp
}
}
}
/// Query parameters for the proxy endpoint
#[derive(Debug, Clone, serde::Deserialize)]
#[allow(missing_docs)]
pub struct ProxyQuery {
pub url: String,
#[serde(flatten)]
pub image_options: ImageOptions,
}
fn deserialize_query_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?.map_or(Ok(None), |s| match s.as_str() {
"true" | "True" | "TRUE" | "1" | "Y" | "yes" | "Yes" | "YES" => Ok(Some(true)),
"false" | "False" | "FALSE" | "0" | "N" | "no" | "No" | "NO" => Ok(Some(false)),
_ => Err(serde::de::Error::custom("expected 'true' or 'false'")),
})
}
/// Query options for the proxy endpoint
#[derive(Debug, Clone, serde::Deserialize)]
pub struct ImageOptions {
/// If set to true, always proxy the image instead of redirecting
#[serde(default, deserialize_with = "deserialize_query_bool")]
pub origin: Option<bool>,
/// See upstream specification
#[serde(default, deserialize_with = "deserialize_query_bool")]
pub avatar: Option<bool>,
/// See upstream specification
#[serde(default, deserialize_with = "deserialize_query_bool")]
#[serde(rename = "static")]
pub static_: Option<bool>,
/// See upstream specification
#[serde(default, deserialize_with = "deserialize_query_bool")]
pub preview: Option<bool>,
/// See upstream specification
#[serde(default, deserialize_with = "deserialize_query_bool")]
pub badge: Option<bool>,
/// See upstream specification
#[serde(default, deserialize_with = "deserialize_query_bool")]
pub emoji: Option<bool>,
/// Set the preferred format, see [`NormalizationPolicy`] for more information
pub format: Option<String>,
}
impl ImageOptions {
/// Convert options to compression level
#[must_use]
pub fn compression_level(&self) -> CompressionLevel {
if self.badge == Some(true) {
return CompressionLevel::Med;
}
if self.avatar == Some(true) {
return CompressionLevel::High;
}
if self.emoji == Some(true) {
return CompressionLevel::Max;
}
if self.preview == Some(true) {
return CompressionLevel::Med;
}
if self.static_ == Some(true) {
return CompressionLevel::Low;
}
CompressionLevel::None
}
/// Whether resizing is requested
#[must_use]
pub fn requested_resize(&self) -> bool {
self.avatar.is_some()
|| self.preview.is_some()
|| self.badge.is_some()
|| self.emoji.is_some()
}
/// Apply preferred format and image type from filename
pub fn apply_filename(&mut self, filename: &str) {
let mut split = filename.split('.');
let stem = split.next().unwrap_or_default();
let ext = split.last().unwrap_or_default();
match ext {
"png" => self.format = Some("png".to_string()),
"jpg" | "jpeg" => self.format = Some("jpeg".to_string()),
"webp" => self.format = Some("webp".to_string()),
"tiff" => self.format = Some("tiff".to_string()),
"gif" => self.format = Some("gif".to_string()),
"bmp" => self.format = Some("bmp".to_string()),
_ => {}
}
match stem {
"avatar" => self.avatar = Some(true),
"static" => self.static_ = Some(true),
"preview" => self.preview = Some(true),
"badge" => self.badge = Some(true),
"emoji" => self.emoji = Some(true),
_ => {}
}
}
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "Serde requires references"
)]
fn serialize_status<S>(status: &StatusCode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u16(status.as_u16())
}
#[derive(Debug, Clone, serde::Serialize)]
/// Error response
#[allow(missing_docs)]
pub struct ErrorResponse {
#[serde(serialize_with = "serialize_status")]
pub status: StatusCode,
pub message: Cow<'static, str>,
}
impl Display for ErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.status, self.message)
}
}
impl std::error::Error for ErrorResponse {}
impl ErrorResponse {
#[cfg(not(feature = "cf-worker"))]
/// URL must be a DNS name
#[must_use]
pub const fn non_dns_name() -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: Cow::Borrowed("URL must be a DNS name"),
}
}
/// Entropy source exhausted
#[must_use]
pub const fn entropy_exhausted() -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: Cow::Borrowed("Entropy source exhausted"),
}
}
/// Worker is configured improperly
#[cfg(feature = "cf-worker")]
#[must_use]
pub fn worker_config_error(err: CfConfigError) -> Self {
match err {
CfConfigError::EnvConversion => Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: Cow::Borrowed("Failed to convert config to JS object"),
},
CfConfigError::JsonParse(e) => Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: format!("Failed to parse config object: {e}").into(),
},
}
}
/// Method not allowed
#[must_use]
pub const fn method_not_allowed() -> Self {
Self {
status: StatusCode::METHOD_NOT_ALLOWED,
message: Cow::Borrowed("Method not allowed"),
}
}
/// Upstream request timed out
#[must_use]
pub const fn upstream_timeout() -> Self {
Self {
status: StatusCode::GATEWAY_TIMEOUT,
message: Cow::Borrowed("Upstream request timed out"),
}
}
/// Unexpected status code
#[must_use]
pub fn unexpected_status(url: &str, status: u16) -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
message: format!("Unexpected status code when accessing {url}: {status}").into(),
}
}
/// Insecure request
#[must_use]
pub const fn insecure_request() -> Self {
Self {
status: StatusCode::FORBIDDEN,
message: Cow::Borrowed("HTTP requests are disabled"),
}
}
/// Rate limit exceeded
#[must_use]
pub const fn rate_limit_exceeded() -> Self {
Self {
status: StatusCode::TOO_MANY_REQUESTS,
message: Cow::Borrowed("Rate limit exceeded"),
}
}
/// Bad URL
#[must_use]
pub const fn bad_url() -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: Cow::Borrowed("Bad URL"),
}
}
/// Upstream sent invalid HTTP response
#[must_use]
pub fn upstream_protocol_error() -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
message: Cow::Borrowed("Upstream protocol error"),
}
}
/// Too many redirects
#[must_use]
pub const fn too_many_redirects() -> Self {
Self {
status: StatusCode::TOO_MANY_REQUESTS,
message: Cow::Borrowed("Too many redirects"),
}
}
/// Cloudflare worker reported an error
#[cfg(feature = "cf-worker")]
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn worker_fetch_error(e: worker::Error) -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
message: Cow::Owned(e.to_string()),
}
}
/// Requested media is too large
#[must_use]
pub const fn loop_detected() -> Self {
Self {
status: StatusCode::LOOP_DETECTED,
message: Cow::Borrowed(
"Loop detected, please make sure your User-Agent and Via headers are not being stripped",
),
}
}
/// Received more data than allowed in one receive
#[must_use]
pub const fn mtu_buffer_overflow() -> Self {
Self {
status: StatusCode::PAYLOAD_TOO_LARGE,
message: Cow::Borrowed("MTU buffer overflow"),
}
}
/// Requested media is too large
#[must_use]
pub const fn payload_too_large() -> Self {
Self {
status: StatusCode::PAYLOAD_TOO_LARGE,
message: Cow::Borrowed("Payload too large"),
}
}
/// Post-processing failed
#[must_use]
pub const fn postprocess_failed(msg: Cow<'static, str>) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: msg,
}
}
/// Requested media is unsafe
#[must_use]
pub const fn unsafe_media() -> Self {
Self {
status: StatusCode::FORBIDDEN,
message: Cow::Borrowed("Unsafe media type"),
}
}
/// Requested media can not be processed
#[must_use]
pub const fn unsupported_media() -> Self {
Self {
status: StatusCode::UNSUPPORTED_MEDIA_TYPE,
message: Cow::Borrowed("Unsupported media type"),
}
}
/// Requested media is not a media file
#[must_use]
pub const fn not_media() -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: Cow::Borrowed("Not a media file"),
}
}
}
#[cfg(feature = "cf-worker")]
impl From<worker::Error> for ErrorResponse {
fn from(e: worker::Error) -> Self {
Self {
status: StatusCode::INTERNAL_SERVER_ERROR,
message: Cow::Owned(e.to_string()),
}
}
}
#[cfg(feature = "reqwest")]
impl From<reqwest::Error> for ErrorResponse {
fn from(e: reqwest::Error) -> Self {
Self {
status: StatusCode::BAD_GATEWAY,
message: Cow::Owned(e.to_string()),
}
}
}
impl IntoResponse for ErrorResponse {
fn into_response(self) -> axum::response::Response {
(self.status, Json(self)).into_response()
}
}
/// Application state
#[allow(unused)]
pub struct AppState<C: UpstreamClient, S: Sandboxing> {
#[cfg(feature = "governor")]
limiters: Box<
[(
RateLimiter<
std::net::IpAddr,
DashMapStateStore<std::net::IpAddr>,
SystemClock,
StateInformationMiddleware,
>,
RwLock<LruCache<std::net::IpAddr, AtomicU64>>,
)],
>,
config: Config,
client: C,
sandbox: S,
}
/// App routes
pub struct App<C: UpstreamClient, S: Sandboxing> {
_marker: PhantomData<(C, S)>,
}
#[cfg(any(feature = "cf-worker", feature = "reqwest"))]
#[allow(clippy::unused_async)]
impl<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static> App<C, S> {
/// Root endpoint
#[cfg_attr(feature = "cf-worker", worker::send)]
pub async fn index(State(state): State<Arc<AppState<Upstream, S>>>) -> Response {
match &state.clone().config.index_redirect {
IndexConfig::Redirect { permanent, ref url } => {
if *permanent {
Redirect::permanent(url).into_response()
} else {
Redirect::temporary(url).into_response()
}
}
IndexConfig::Message(msg) => (StatusCode::OK, msg.to_string()).into_response(),
}
}
#[cfg_attr(feature = "cf-worker", worker::send)]
async fn proxy_impl<'a>(
method: http::Method,
filename: Option<&str>,
State(state): State<Arc<AppState<Upstream, S>>>,
Query(query): Query<ProxyQuery>,
info: IncomingInfo,
) -> Result<Response, ErrorResponse>
where
<<C as UpstreamClient>::Response as HTTPResponse>::BodyStream: Unpin,
{
let mut options = query.image_options;
if let Some(filename) = filename {
options.apply_filename(filename);
}
match method {
http::Method::GET => {}
http::Method::HEAD => {
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" | "jpg") => "image/jpeg",
Some("webp") => "image/webp",
_ => "image/webp",
}
.parse()
.unwrap(),
);
return Ok(resp);
}
_ => {
return Err(ErrorResponse::method_not_allowed());
}
}
log::info!("Proxying {}, options: {:?}", query.url, options);
let resp = state
.client
.request_upstream(&info, &query.url, false, true, DEFAULT_MAX_REDIRECTS)
.await?;
let media = Box::pin(MediaResponse::from_upstream_response::<S>(
resp,
state.config.allow_unknown,
&state.config.post_process,
options,
&state.sandbox,
))
.await?;
// Just in case we proxied something we shouldn't
let mut resp = media.into_response();
resp.headers_mut().insert(
"Content-Security-Policy",
"default-src 'none'; img-src 'self'".parse().unwrap(),
);
Ok(resp)
}
/// Proxy endpoint without filename
#[cfg_attr(feature = "cf-worker", worker::send)]
pub async fn proxy_without_filename(
method: http::Method,
Query(query): Query<ProxyQuery>,
State(state): State<Arc<AppState<Upstream, S>>>,
info: IncomingInfo,
) -> Result<Response, ErrorResponse>
where
<<C as UpstreamClient>::Response as HTTPResponse>::BodyStream: Unpin,
{
Self::proxy_impl(method, None, State(state), Query(query), info).await
}
/// Proxy OPTIONS endpoint
#[cfg_attr(feature = "cf-worker", worker::send)]
pub async fn proxy_options() -> HeaderMap {
let mut hm = HeaderMap::new();
hm.insert(
"Access-Control-Allow-Methods",
"GET, OPTIONS".parse().unwrap(),
);
hm
}
/// Proxy endpoint with filename
#[cfg_attr(feature = "cf-worker", worker::send)]
pub async fn proxy_with_filename(
method: http::Method,
Path(filename): Path<String>,
State(state): State<Arc<AppState<Upstream, S>>>,
Query(query): Query<ProxyQuery>,
info: IncomingInfo,
) -> Result<Response, ErrorResponse>
where
<<C as UpstreamClient>::Response as HTTPResponse>::BodyStream: Unpin,
{
Self::proxy_impl(method, Some(&filename), State(state), Query(query), info).await
}
}