use crate::server::middleware::apply_custom_to_router;
use crate::server::{OrdinaryAppRouter, OrdinaryAppServerState};
use async_recursion::async_recursion;
use axum::extract::{Path, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::{Router, routing::get};
use ordinary_config::{CompressionAlgorithm, OrdinaryConfig};
use ordinary_server_utils::GMT_FORMAT;
use ordinary_server_utils::middleware::check_if_none_match;
use std::sync::Arc;
use time::{Duration, UtcDateTime};
use tracing::Instrument;
pub(crate) fn setup_router(
config: &Arc<OrdinaryConfig>,
state: &Arc<OrdinaryAppServerState>,
host_domain: Option<&str>,
forwarded_by: &str,
forwarded_proto: &str,
) -> Option<OrdinaryAppRouter> {
if let Some(assets) = &config.assets {
let mut router = Router::new();
let base_route = if assets.base_route == "/" {
""
} else {
assets.base_route.as_str()
};
router = router.route(&format!("{base_route}/{{*path}}"), get(get_asset));
if assets.append_index_html == Some(true) && assets.skip_base_route_index_html != Some(true)
{
if base_route.is_empty() {
router = router.route("/", get(get_asset));
} else {
router = router
.route(base_route, get(get_asset))
.route(&format!("{base_route}/"), get(get_asset));
}
}
if let Some(names) = &assets.middlewares {
router = apply_custom_to_router(
router,
config,
state,
names,
config.domain.clone(),
forwarded_by.to_string(),
forwarded_proto.to_string(),
host_domain,
);
}
Some(router)
} else {
None
}
}
pub(crate) async fn get_asset(
State(state): State<Arc<OrdinaryAppServerState>>,
path: Option<Path<String>>,
headers: HeaderMap,
) -> impl IntoResponse {
get_asset_to_res(state, path.map(|p| p.0), &headers, false)
.await
.into_response()
}
#[allow(
clippy::needless_pass_by_value,
clippy::too_many_lines,
clippy::too_many_arguments
)]
#[async_recursion]
pub(crate) async fn get_asset_to_res(
state: Arc<OrdinaryAppServerState>,
path: Option<String>,
headers: &HeaderMap,
bail: bool,
) -> impl IntoResponse {
let span = tracing::info_span!("storage");
let span = span.in_scope(|| tracing::info_span!("assets"));
async {
let Some(assets_config) = &state.config.assets else {
tracing::warn!("no assets config");
return StatusCode::NOT_FOUND.into_response();
};
let append_index_html = assets_config.append_index_html.unwrap_or(false);
let append_html_ext = assets_config.append_html_ext.unwrap_or(false);
let mut path = path.unwrap_or_default();
let mut ext = path.rsplit_once('.').map(|(_, ext)| ext);
if append_index_html {
if path.is_empty() || path.ends_with('/') {
path.push_str("index.html");
ext = Some("html");
} else if ext.is_none() {
path.push_str("/index.html");
ext = Some("html");
}
}
let ext = if let Some(ext) = ext {
ext
} else if append_html_ext {
path.push_str(".html");
"html"
} else {
""
};
let no_compress = matches!(
ext,
"otf"
| "ttf"
| "woff"
| "woff2"
| "png"
| "apng"
| "gif"
| "jpg"
| "jpeg"
| "bmp"
| "tif"
| "tiff"
| "webp"
| "avif"
| "ico"
| "pdf"
);
let mut skip_check = false;
if !no_compress
&& let Some(precompression) = &assets_config.internal_precompression
&& let Some(compressions) = headers.get(header::ACCEPT_ENCODING)
&& let Ok(compressions_str) = compressions.to_str()
{
for alg in precompression {
if compressions_str.contains(alg.as_str()) {
if let Some(value) =
get_asset_with_compression(&state, &path, headers, Some(alg), ext == "html")
{
return value;
}
skip_check = true;
break;
}
}
}
if !skip_check
&& let Some(value) =
get_asset_with_compression(&state, &path, headers, None, ext == "html")
{
return value;
}
if !bail
&& let Some(error_config) = &state.config.error
&& let Some(asset_name) = &error_config.asset
{
return get_asset_to_res(state.clone(), Some(asset_name.clone()), headers, true)
.await
.into_response();
}
StatusCode::NOT_FOUND.into_response()
}
.instrument(span)
.await
}
fn get_asset_with_compression(
state: &Arc<OrdinaryAppServerState>,
path: &str,
headers: &HeaderMap,
compression: Option<&CompressionAlgorithm>,
html_csp: bool,
) -> Option<Response> {
if let Ok(asset) = state.storage.asset.get(path, compression)
&& let Ok(reader) = flexbuffers::Reader::get_root(asset.as_ref())
{
let vec = reader.as_vector();
let etag = vec.idx(2).as_str();
let last_modified = vec.idx(3).as_str();
let mut header_map = HeaderMap::with_capacity(11);
if html_csp {
header_map.insert(
header::CONTENT_SECURITY_POLICY,
state.html_asset_csp.clone(),
);
header_map.insert(
state.html_asset_reporting_endpoints.0.clone(),
state.html_asset_reporting_endpoints.1.clone(),
);
}
header_map.insert(
header::VARY,
HeaderValue::from_static(header::ACCEPT_ENCODING.as_str()),
);
if let Ok(etag) = HeaderValue::from_str(etag) {
header_map.insert(header::ETAG, etag);
}
if let Ok(last_modified) = HeaderValue::from_str(last_modified) {
header_map.insert(header::LAST_MODIFIED, last_modified);
}
if let Some(assets) = &state.config.assets {
if let Some(http_cache_control) = &assets.internal_cache_control_header_value
&& let Ok(cache_control) = HeaderValue::from_str(http_cache_control.as_str())
{
header_map.insert(header::CACHE_CONTROL, cache_control);
}
if let Some(http_cache) = &assets.http
&& let Some(expires_s) = http_cache.expires
{
let future = UtcDateTime::now() + Duration::seconds(expires_s.cast_signed());
if let Ok(formatted) = future.format(&GMT_FORMAT)
&& let Ok(expires) = HeaderValue::from_str(formatted.as_str())
{
header_map.insert(header::EXPIRES, expires);
}
}
}
if let Some(etag) = check_if_none_match(headers, etag)
&& let Ok(etag_header) = HeaderValue::from_str(etag)
{
header_map.insert(header::ETAG, etag_header);
return Some((StatusCode::NOT_MODIFIED, header_map).into_response());
} else if let Some(if_modified_since) = headers.get(header::IF_MODIFIED_SINCE)
&& let Ok(if_modified_since_str) = if_modified_since.to_str()
&& let Ok(if_modified_since) = UtcDateTime::parse(if_modified_since_str, &GMT_FORMAT)
&& let Ok(last_modified) = UtcDateTime::parse(last_modified, &GMT_FORMAT)
&& if_modified_since >= last_modified
{
return Some((StatusCode::NOT_MODIFIED, header_map).into_response());
}
let mime = vec.idx(0).as_str();
if let Ok(mime) = HeaderValue::from_str(mime) {
header_map.insert(CONTENT_TYPE, mime);
}
if let Some(compression) = compression {
header_map.insert(
header::CONTENT_ENCODING,
HeaderValue::from_static(compression.as_str()),
);
}
return Some(
(
StatusCode::OK,
header_map,
bytes::Bytes::copy_from_slice(vec.idx(1).as_blob().0),
)
.into_response(),
);
}
None
}