use axum::{
http::{
HeaderMap, HeaderName, HeaderValue, StatusCode,
header::{CONTENT_ENCODING, CONTENT_TYPE, ETAG, IF_NONE_MATCH},
},
response::{IntoResponse, Response},
};
use tracing::debug;
use crate::{
options::ServeOptions,
util::{
compression::{compress_brotli, compress_gzip, decompress_brotli},
headers::{content_length, supports_encoding},
},
};
const BROTLI_ENCODING: &str = "br";
const BROTLI_HEADER: (HeaderName, HeaderValue) =
(CONTENT_ENCODING, HeaderValue::from_static(BROTLI_ENCODING));
const GZIP_ENCODING: &str = "gzip";
const GZIP_HEADER: (HeaderName, HeaderValue) =
(CONTENT_ENCODING, HeaderValue::from_static(GZIP_ENCODING));
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OnDemandEncoding {
Identity,
Brotli,
Gzip,
}
#[derive(Debug)]
pub struct Asset {
pub route: &'static str,
pub path: &'static str,
pub etag: &'static str,
pub content_type: &'static str,
pub bytes: Option<&'static [u8]>,
pub is_compressed: bool,
pub should_compress: bool,
}
struct AssetResponse<'t, B> {
options: &'t ServeOptions,
headers: &'t HeaderMap,
status: StatusCode,
asset: &'t Asset,
etag: &'t str,
bytes: B,
bytes_len: usize,
brotli_bytes: B,
brotli_bytes_len: usize,
gzip_bytes: B,
gzip_bytes_len: usize,
}
impl<B: IntoResponse> AssetResponse<'_, B> {
fn into_response(self) -> Response {
let content_type = self.asset.content_type();
let cache_control = self.asset.cache_control(self.options);
let etag_header = (ETAG, HeaderValue::from_str(self.etag).unwrap());
if let Some(if_none_match) = self.headers.get(IF_NONE_MATCH)
&& if_none_match == self.etag
{
return (
StatusCode::NOT_MODIFIED,
[content_type, cache_control, etag_header],
)
.into_response();
}
if self.options.enable_brotli
&& self.brotli_bytes_len > 0
&& supports_encoding(self.headers, BROTLI_ENCODING)
{
return (
self.status,
[
content_length(self.brotli_bytes_len),
BROTLI_HEADER,
content_type,
cache_control,
etag_header,
],
self.brotli_bytes,
)
.into_response();
}
if self.options.enable_gzip
&& self.gzip_bytes_len > 0
&& supports_encoding(self.headers, GZIP_ENCODING)
{
return (
self.status,
[
content_length(self.gzip_bytes_len),
GZIP_HEADER,
content_type,
cache_control,
etag_header,
],
self.gzip_bytes,
)
.into_response();
}
(
self.status,
[
content_length(self.bytes_len),
content_type,
cache_control,
etag_header,
],
self.bytes,
)
.into_response()
}
}
impl Asset {
fn cache_control(&self, options: &ServeOptions) -> (HeaderName, HeaderValue) {
match self.content_type {
"text/html" => options.html_cache_control.as_header(),
_ => options.cache_control.as_header(),
}
}
fn content_type(&self) -> (HeaderName, HeaderValue) {
(CONTENT_TYPE, HeaderValue::from_static(self.content_type))
}
pub(crate) fn leak_bytes(
&self,
options: &'static ServeOptions,
) -> (&'static [u8], &'static [u8], &'static [u8]) {
let mut uncompressed = self.bytes.unwrap_or_default();
if self.is_compressed {
uncompressed = Box::new(decompress_brotli(uncompressed).unwrap_or_default()).leak()
}
let gzip_bytes = if self.should_compress && options.enable_gzip {
Box::new(compress_gzip(uncompressed).unwrap_or_default()).leak()
} else {
Default::default()
};
let brotli_bytes = if self.should_compress && options.enable_brotli {
self.bytes.unwrap_or_default()
} else {
Default::default()
};
(uncompressed, brotli_bytes, gzip_bytes)
}
fn read_source_bytes(&self) -> Result<Vec<u8>, StatusCode> {
std::fs::read(self.path).map_err(|_| StatusCode::NOT_FOUND)
}
fn negotiate_dynamic_encoding(
&self,
headers: &HeaderMap,
options: &ServeOptions,
) -> OnDemandEncoding {
if !self.should_compress {
return OnDemandEncoding::Identity;
}
if options.enable_brotli && supports_encoding(headers, BROTLI_ENCODING) {
return OnDemandEncoding::Brotli;
}
if options.enable_gzip && supports_encoding(headers, GZIP_ENCODING) {
return OnDemandEncoding::Gzip;
}
OnDemandEncoding::Identity
}
fn encode_dynamic_bytes(&self, bytes: &[u8], encoding: OnDemandEncoding) -> (Vec<u8>, Vec<u8>) {
match encoding {
OnDemandEncoding::Brotli => (compress_brotli(bytes).unwrap_or_default(), Vec::new()),
OnDemandEncoding::Gzip => (Vec::new(), compress_gzip(bytes).unwrap_or_default()),
OnDemandEncoding::Identity => (Vec::new(), Vec::new()),
}
}
fn dynamic_handler(
&self,
headers: &HeaderMap,
status: StatusCode,
options: &ServeOptions,
) -> Response {
let bytes = match self.read_source_bytes() {
Ok(bytes) => bytes,
Err(status) => return status.into_response(),
};
let encoding = self.negotiate_dynamic_encoding(headers, options);
let (brotli_bytes, gzip_bytes) = self.encode_dynamic_bytes(&bytes, encoding);
let etag = sha256::digest(&bytes);
AssetResponse {
options,
headers,
status,
asset: self,
etag: &etag,
bytes_len: bytes.len(),
bytes,
brotli_bytes_len: brotli_bytes.len(),
brotli_bytes,
gzip_bytes_len: gzip_bytes.len(),
gzip_bytes,
}
.into_response()
}
pub(super) fn handler(
&self,
headers: &HeaderMap,
status: StatusCode,
bytes: &'static [u8],
brotli_bytes: &'static [u8],
gzip_bytes: &'static [u8],
options: &ServeOptions,
) -> Response {
if bytes.is_empty() {
debug!("using dynamic handler for {}", self.path);
return self.dynamic_handler(headers, status, options);
}
AssetResponse {
options,
headers,
status,
asset: self,
etag: self.etag,
bytes_len: bytes.len(),
bytes,
brotli_bytes_len: brotli_bytes.len(),
brotli_bytes,
gzip_bytes_len: gzip_bytes.len(),
gzip_bytes,
}
.into_response()
}
}