use crate::server::ops::assets::get_asset_to_res;
use arrayvec::ArrayVec;
use async_compression::Level;
use axum::body::Bytes;
use axum::extract::{MatchedPath, Path, Request, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;
use base64::{Engine as B64Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use hyper::HeaderMap;
use ordinary_config::{CompressionAlgorithm, StoredCache};
use ordinary_storage::{CacheCompression, CacheDependency, CacheRead, CacheWrite};
use ordinary_template::{Template, TemplateResult};
use ordinary_utils::compression::get_compressed;
use ordinary_utils::middleware::{check_if_none_match, get_etag_hash};
use ordinary_utils::{GMT_FORMAT, get_host};
use std::sync::Arc;
use time::{Duration, UtcDateTime};
use tracing::{Instrument, Span};
fn insert_headers(
template: &Template,
header_map: &mut HeaderMap,
etag: &str,
last_modified: &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(cache_control) = &template.cache_control
&& let Ok(cache_control) = HeaderValue::from_str(cache_control.as_str())
{
header_map.insert(header::CACHE_CONTROL, cache_control);
}
if let Some(cache) = &template.config.cache
&& let Some(http_cache) = &cache.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);
}
}
}
#[allow(clippy::too_many_lines)]
pub async fn get(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
jar: CookieJar,
matched_path: MatchedPath,
req: Request,
) -> Result<impl IntoResponse, (StatusCode, Redirect)> {
let span = tracing::info_span!(
"template",
i = tracing::field::Empty,
nm = tracing::field::Empty,
);
let uri = req.uri();
let headers = req.headers();
let path_and_query = req.uri().path();
async {
let Some(host) = get_host(headers, uri) else {
tracing::error!("no host");
return Ok(StatusCode::BAD_REQUEST.into_response());
};
let params = uri.query().map(ToString::to_string);
let token: Vec<u8>;
let mut claims = None;
if let Some(idx) = state.template_route_map.get(matched_path.as_str()) {
if let Some(template) = state.templates.get(*idx) {
span.record("i", template.idx);
span.record("nm", tracing::field::display(&template.config.name));
if let Some(_check) = &template.config.protected {
let cookie_name = if state.secure_cookies {
"__Host-ORDINARY-ACCESS-TOKEN"
} else {
"ORDINARY-ACCESS-TOKEN"
};
token = if let Some(token) = jar.get(cookie_name) {
if let Ok(token) = b64.decode(token.value()) {
token
} else {
return Err((
StatusCode::UNAUTHORIZED,
Redirect::to("/accounts/access/redirect"),
));
}
} else {
return Err((
StatusCode::UNAUTHORIZED,
Redirect::to("/accounts/access/redirect"),
));
};
match state.auth.verify_access_token(&token) {
Ok((_account, real_claims)) => {
claims = Some(real_claims);
}
Err(_) => {
return Err((
StatusCode::UNAUTHORIZED,
Redirect::to("/accounts/access/redirect"),
));
}
}
}
let mut compression_opts = ArrayVec::<CacheCompression, 4>::new();
let mut header_map = HeaderMap::with_capacity(11);
insert_base_headers(template, &mut header_map);
let cache_span = tracing::info_span!("cache");
if let Some(cache) = &template.config.cache
&& let Some(stored_cache) = &cache.stored
{
set_compression_opts(headers, &mut compression_opts, &cache_span, stored_cache);
if let Ok(Some((hit, compression))) = async {
state
.storage
.cache
.check(
&CacheRead::Template,
&compression_opts,
template.idx,
path_and_query,
)
.await
}
.instrument(cache_span.clone())
.await
&& let Ok(root) = flexbuffers::Reader::get_root(hit.as_ref())
{
let root_vec = root.as_vector();
let etag = root_vec.idx(1).as_str();
let last_modified = root_vec.idx(2).as_str();
let res = root_vec.idx(3).as_blob().0;
return Ok(process_response(
true,
headers,
template,
header_map,
compression,
etag,
last_modified,
res,
)
.into_response());
}
}
{
match template.render(
host.as_str(),
path_and_query.into(),
params.clone(),
None,
None,
&claims,
) {
Ok(res) => {
let mut res = match res {
TemplateResult::Result(bytes) => bytes,
TemplateResult::StatusCode(code) => return Ok(code.into_response()),
};
let mut etag = cache_span.in_scope(|| {
if let Some(config) = &template.config.cache
&& let Some(http_cache) = &config.http
{
get_etag_hash(res.as_ref(), Some(http_cache))
} else {
get_etag_hash(res.as_ref(), None)
}
});
let Ok(last_modified) = UtcDateTime::now().format(&GMT_FORMAT) else {
return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
};
let last_modified_str = last_modified.as_str();
let mut cache_compression = None;
if let Some(cache) = &template.config.cache
&& let Some(stored_cache) = &cache.stored
{
async {
if let Some(compression) = compression_opts.first() {
res = get_compressed_res(&res, compression).await;
cache_compression = Some(compression);
etag.push(compression.as_char());
}
if let Err(err) = state
.storage
.cache
.write(
CacheWrite::Template(
etag.as_str(),
last_modified_str,
&res,
),
cache_compression,
template.idx,
stored_cache,
path_and_query,
if template.config.content.is_some() {
vec![CacheDependency::Content]
} else {
vec![]
},
)
.await
{
tracing::error!(%err, "failed to write to cache");
}
}
.instrument(cache_span)
.await;
}
return Ok(process_response(
false,
headers,
template,
header_map,
cache_compression,
etag.as_str(),
last_modified_str,
res.as_ref(),
)
.into_response());
}
Err(err) => tracing::error!("{err}"),
}
}
}
} else {
tracing::warn!("no template for route '{:?}'", matched_path);
}
if let Some(idx) = state.error_template_idx
&& let Some(err_template) = state.templates.get(idx as usize)
{
match err_template.render(
host.as_str(),
"/error".into(),
params,
Some(("no template for this route".into(), 404)),
None,
&claims,
) {
Ok(res) => {
return match res {
TemplateResult::Result(bytes) => Ok((
StatusCode::NOT_FOUND,
[(CONTENT_TYPE, err_template.mime.clone())],
bytes,
)
.into_response()),
TemplateResult::StatusCode(code) => return Ok(code.into_response()),
};
}
Err(err) => tracing::error!("{err}"),
}
} else if let Some(error_config) = &state.config.error
&& let Some(asset_name) = &error_config.asset
{
return Ok(get_asset_to_res(
state.clone(),
Some(asset_name.clone()),
uri.clone(),
headers.clone(),
true,
)
.into_response());
}
Ok((
StatusCode::NOT_FOUND,
[(CONTENT_TYPE, "text/plain")],
Bytes::copy_from_slice(b"no template for the specified route"),
)
.into_response())
}
.instrument(span.clone())
.await
}
async fn get_compressed_res(res: &Bytes, compression: &CacheCompression) -> Bytes {
match compression {
CacheCompression::Zstd { level } => {
get_compressed(
res.as_ref(),
compression.as_str(),
Some(Level::Precise(i32::from(*level))),
)
.await
}
_ => get_compressed(res.as_ref(), compression.as_str(), None).await,
}
}
#[allow(clippy::too_many_arguments)]
fn process_response(
check_last_modified: bool,
headers: &HeaderMap,
template: &Template,
mut header_map: HeaderMap,
compression: Option<&CacheCompression>,
etag: &str,
last_modified: &str,
res: &[u8],
) -> impl IntoResponse {
insert_headers(template, &mut header_map, etag, last_modified);
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);
(StatusCode::NOT_MODIFIED, header_map).into_response()
} else if check_last_modified
&& 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
{
(StatusCode::NOT_MODIFIED, header_map).into_response()
} else {
if let Some(compression) = compression {
header_map.insert(
header::CONTENT_ENCODING,
HeaderValue::from_static(compression.as_str()),
);
}
header_map.insert(CONTENT_TYPE, template.mime.clone());
(StatusCode::OK, header_map, Bytes::copy_from_slice(res)).into_response()
}
}
fn set_compression_opts(
headers: &HeaderMap,
compression_opts: &mut ArrayVec<CacheCompression, 4>,
cache_span: &Span,
stored_cache: &StoredCache,
) {
cache_span.in_scope(|| {
if let Some(compression) = &stored_cache.internal_compression
&& let Some(compressions) = headers.get(header::ACCEPT_ENCODING)
&& let Ok(compressions_str) = compressions.to_str()
{
for compression in compression {
match compression {
CompressionAlgorithm::Brotli => {
if compressions_str.contains("br") {
compression_opts.push(CacheCompression::Brotli);
}
}
CompressionAlgorithm::Deflate => {
if compressions_str.contains("deflate") {
compression_opts.push(CacheCompression::Deflate);
}
}
CompressionAlgorithm::Zstd { level } => {
if compressions_str.contains("zstd") {
compression_opts.push(CacheCompression::Zstd { level: *level });
}
}
CompressionAlgorithm::Gzip => {
if compressions_str.contains("gzip") {
compression_opts.push(CacheCompression::Gzip);
}
}
CompressionAlgorithm::All => {
tracing::error!("should not be able to hit 'CompressionAlgorithm::All'");
}
}
}
}
});
}
fn insert_base_headers(template: &Template, header_map: &mut HeaderMap) {
{
let csp_lock = template.csp.read();
if let Some(csp) = csp_lock.as_ref() {
header_map.insert(
template.reporting_endpoints.0.clone(),
template.reporting_endpoints.1.clone(),
);
header_map.insert(header::CONTENT_SECURITY_POLICY, csp.to_owned());
}
}
header_map.insert(
header::VARY,
HeaderValue::from_static(header::ACCEPT_ENCODING.as_str()),
);
}
pub async fn query(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
Path((idx, path)): Path<(u8, String)>,
req: Request,
) -> impl IntoResponse {
let span = tracing::info_span!(
"template",
i = tracing::field::Empty,
nm = tracing::field::Empty,
);
let uri = req.uri();
let headers = req.headers();
let query = req.uri().query();
async {
let Some(host) = get_host(headers, uri) else {
tracing::error!("no host");
return StatusCode::BAD_REQUEST.into_response();
};
let token: Vec<u8>;
let mut claims = None;
if let Some(template) = state.templates.get(idx as usize) {
span.record("i", template.idx);
span.record("nm", tracing::field::display(&template.config.name));
if let Some(_check) = &template.config.protected
&& let Some(val) = headers.get("authorization")
&& let Ok(str_val) = val.to_str()
&& let Some(b64_token) = str_val.strip_prefix("Bearer ")
&& let Ok(t) = b64.decode(b64_token)
{
token = t;
match state.auth.verify_access_token(&token) {
Ok((_account, real_claims)) => {
claims = Some(real_claims);
}
Err(err) => {
tracing::error!("{err}");
return (
StatusCode::UNAUTHORIZED,
Bytes::copy_from_slice(b"unauthorized"),
)
.into_response();
}
}
}
let params = query.map(ToString::to_string);
match template.query(host.as_str(), path, params, None, None, &claims) {
Ok(res) => {
let mut header_map = HeaderMap::with_capacity(11);
{
let csp_lock = template.csp.read();
if let Some(csp) = csp_lock.as_ref() {
header_map.insert(
template.reporting_endpoints.0.clone(),
template.reporting_endpoints.1.clone(),
);
header_map.insert(header::CONTENT_SECURITY_POLICY, csp.to_owned());
}
}
header_map.insert(
header::VARY,
HeaderValue::from_static(header::ACCEPT_ENCODING.as_str()),
);
let etag = if let Some(config) = &template.config.cache
&& let Some(http_cache) = &config.http
{
get_etag_hash(res.as_ref(), Some(http_cache))
} else {
get_etag_hash(res.as_ref(), None)
};
let etag_str = etag.as_str();
let Ok(last_modified) = UtcDateTime::now().format(&GMT_FORMAT) else {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
};
insert_headers(template, &mut header_map, etag_str, last_modified.as_str());
return if let Some(etag) = check_if_none_match(headers, etag_str)
&& let Ok(etag_header) = HeaderValue::from_str(etag)
{
header_map.insert(header::ETAG, etag_header);
(StatusCode::NOT_MODIFIED, header_map).into_response()
} else {
(StatusCode::OK, header_map, res).into_response()
};
}
Err(err) => {
tracing::error!("{err}");
}
}
} else {
tracing::warn!("no template for index '{idx}'");
}
(StatusCode::NOT_FOUND, Bytes::copy_from_slice(b"not found")).into_response()
}
.instrument(span.clone())
.await
}