use crate::server::OrdinaryAppServerState;
use crate::server::ops::assets::get_asset_to_res;
use arrayvec::ArrayVec;
use async_compression::Level;
use axum::body::Bytes;
use axum::extract::{MatchedPath, Request, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderValue, StatusCode, Uri, 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 bytes::{BufMut, BytesMut};
use flexbuffers::VectorReader;
use hyper::HeaderMap;
use ordinary_config::{CompressionAlgorithm, StoredCache};
use ordinary_storage::{CacheDependency, CacheKind};
use ordinary_template::query::cache::get_cache_key;
use ordinary_template::{OrdinaryTemplate, TemplateResult};
use ordinary_utils::compression::get_compressed;
use ordinary_utils::middleware::{check_if_none_match, get_etag_hash};
use ordinary_utils::tcp::Sni;
use ordinary_utils::{GMT_FORMAT, get_host_fwd};
use smallvec::SmallVec;
use std::sync::Arc;
use time::{Duration, UtcDateTime};
use tracing::{Instrument, Span};
fn insert_headers(
template: &OrdinaryTemplate,
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<OrdinaryAppServerState>>,
jar: CookieJar,
matched_path: MatchedPath,
req: Request,
) -> Result<impl IntoResponse, (StatusCode, Redirect)> {
let uri = req.uri();
let headers = req.headers();
let path = req.uri().path();
let sni = req.extensions().get::<Sni>();
let Some(host) = get_host_fwd(headers, uri, sni) else {
tracing::error!("no host");
return Ok(StatusCode::BAD_REQUEST.into_response());
};
let params = uri.query().map(ToString::to_string);
if let Some(idx) = state.template_route_map.get(matched_path.as_str()) {
if let Some(template) = state.templates.get(*idx) {
let span = tracing::info_span!("tmp", i = template.idx, nm = %template.config.name_validated());
return async {
let token: Vec<u8>;
let mut claims = None;
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::<CompressionAlgorithm, 4>::new();
let mut header_map = HeaderMap::with_capacity(11);
let mut cache_key = String::new();
let route_segments: SmallVec<[&str; 9]> = path.split('/').collect();
let mut qs_params = None;
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_config) = &cache.stored
{
let (ck, qsp) =
match get_cache_key(template, &host, &route_segments, params.as_deref()) {
Ok(val) => val,
Err(err) => {
tracing::error!(%err);
return Ok(StatusCode::BAD_REQUEST.into_response());
}
};
cache_key = ck;
qs_params = qsp;
set_compression_opts(
headers,
&mut compression_opts,
&cache_span,
Some(stored_cache_config),
);
let mut compression = None;
let cache_res = async {
let mut hit = None;
if compression_opts.is_empty() {
if hit.is_none()
&& let Ok(res) = state
.storage
.cache
.check(
stored_cache_config,
&CacheKind::Template,
template.idx,
Bytes::copy_from_slice(cache_key.as_bytes()),
)
.await
&& res.is_some()
{
hit = res;
}
} else {
for comp in &compression_opts {
let mut cache_key = BytesMut::from(cache_key.as_bytes());
cache_key.put_u8(comp.as_u8());
if let Ok(res) = state
.storage
.cache
.check(
stored_cache_config,
&CacheKind::Template,
template.idx,
cache_key.into(),
)
.await
&& res.is_some()
{
hit = res;
compression = Some(comp);
break;
}
}
}
hit
}
.instrument(cache_span.clone())
.await;
if let Some(hit) = cache_res
&& let Ok(root) = flexbuffers::Reader::get_root(hit.as_ref())
{
let root_vec = root.as_vector();
let etag = root_vec.idx(0).as_str();
let last_modified = root_vec.idx(1).as_str();
let res = root_vec.idx(2).as_blob().0;
return Ok(process_response(
true,
headers,
template,
header_map,
compression,
etag,
last_modified,
res,
)
.into_response());
}
} else {
set_compression_opts(headers, &mut compression_opts, &cache_span, None);
}
{
match template
.render(
host.as_str(),
path,
Some(&route_segments),
params.as_ref(),
qs_params,
None,
None,
claims.as_ref(),
)
.await
{
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 compression = None;
if let Some(comp) = compression_opts.first() {
res = get_compressed_res(&res, comp).await;
compression = Some(comp);
etag.push(comp.as_char());
}
if let Some(cache) = &template.config.cache
&& let Some(stored_cache_config) = &cache.stored
{
let mut builder = flexbuffers::Builder::new(
&flexbuffers::BuilderOptions::SHARE_NONE,
);
let mut builder_vec = builder.start_vector();
builder_vec.push(etag.as_str());
builder_vec.push(last_modified_str);
builder_vec.push(flexbuffers::Blob(res.as_ref()));
builder_vec.end_vector();
let mut cache_key = BytesMut::from(cache_key.as_bytes());
if let Some(comp) = compression {
cache_key.put_u8(comp.as_u8());
}
let state_clone = state.clone();
let stored_cache_config_clone = stored_cache_config.clone();
let template_idx = template.idx;
let dep_arr = if template.config.content.is_some() {
vec![CacheDependency::Content]
} else {
vec![]
};
tokio::spawn(async move {
async {
if let Err(err) = state_clone
.storage
.cache
.write(
&stored_cache_config_clone,
CacheKind::Template,
template_idx,
cache_key.into(),
builder.view(),
dep_arr,
)
.await
{
tracing::error!(%err, "failed to write to cache");
}
}
.instrument(cache_span)
.await;
});
}
Ok(process_response(
false,
headers,
template,
header_map,
compression,
etag.as_str(),
last_modified_str,
res.as_ref(),
)
.into_response())
}
Err(err) => {
tracing::error!("{err}");
if let Some(value) = respond_error(
&state,
uri,
headers,
sni,
&host,
params.as_ref(),
claims.as_ref(),
)
.await
{
return Ok(value.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;
}
} else {
tracing::warn!("no template for route '{:?}'", matched_path);
}
if let Some(value) =
respond_error(&state, uri, headers, sni, &host, params.as_ref(), None).await
{
return Ok(value.into_response());
}
Ok((
StatusCode::NOT_FOUND,
[(CONTENT_TYPE, "text/plain")],
Bytes::copy_from_slice(b"no template for the specified route"),
)
.into_response())
}
async fn respond_error(
state: &Arc<OrdinaryAppServerState>,
uri: &Uri,
headers: &HeaderMap,
sni: Option<&Sni>,
host: &str,
params: Option<&String>,
claims: Option<&VectorReader<&[u8]>>,
) -> Option<impl IntoResponse> {
if let Some(idx) = state.error_template_idx
&& let Some(err_template) = state.templates.get(idx as usize)
{
match err_template
.render(
host,
"/error",
None,
params,
None,
Some(("no template for this route".into(), 404)),
None,
claims,
)
.await
{
Ok(res) => {
return Some(match res {
TemplateResult::Result(bytes) => (
StatusCode::NOT_FOUND,
[(CONTENT_TYPE, err_template.mime.clone())],
bytes,
)
.into_response(),
TemplateResult::StatusCode(code) => return Some(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 Some(
get_asset_to_res(
state.clone(),
Some(asset_name.clone()),
uri,
headers,
sni,
true,
)
.await
.into_response(),
);
}
None
}
async fn get_compressed_res(res: &Bytes, compression: &CompressionAlgorithm) -> Bytes {
match compression {
CompressionAlgorithm::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: &OrdinaryTemplate,
mut header_map: HeaderMap,
compression: Option<&CompressionAlgorithm>,
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<CompressionAlgorithm, 4>,
cache_span: &Span,
stored_cache: Option<&StoredCache>,
) {
cache_span.in_scope(|| {
if let Some(stored_cache) = stored_cache {
if let Some(compression) = &stored_cache.internal_compression
&& let Some(compressions) = headers.get(header::ACCEPT_ENCODING)
&& let Ok(compressions_str) = compressions.to_str()
{
let split_compression = compressions_str.split(", ").collect::<ArrayVec<_, 4>>();
for compression in compression {
match compression {
CompressionAlgorithm::Brotli => {
if split_compression.contains(&"br") {
compression_opts.push(compression.to_owned());
}
}
CompressionAlgorithm::Deflate => {
if split_compression.contains(&"deflate") {
compression_opts.push(compression.to_owned());
}
}
CompressionAlgorithm::Zstd { level: _ } => {
if split_compression.contains(&"zstd") {
compression_opts.push(compression.to_owned());
}
}
CompressionAlgorithm::Gzip => {
if split_compression.contains(&"gzip") {
compression_opts.push(compression.to_owned());
}
}
CompressionAlgorithm::All => {
tracing::error!(
"should not be able to hit 'CompressionAlgorithm::All'"
);
}
}
}
}
} else if let Some(compressions) = headers.get(header::ACCEPT_ENCODING)
&& let Ok(compressions_str) = compressions.to_str()
{
let split_compression = compressions_str.split(", ").collect::<ArrayVec<_, 4>>();
if split_compression.contains(&"deflate") {
compression_opts.push(CompressionAlgorithm::Deflate);
return;
}
if split_compression.contains(&"zstd") {
compression_opts.push(CompressionAlgorithm::Zstd { level: 4 });
return;
}
if split_compression.contains(&"gzip") {
compression_opts.push(CompressionAlgorithm::Gzip);
return;
}
if split_compression.contains(&"br") {
compression_opts.push(CompressionAlgorithm::Brotli);
}
}
});
}
fn insert_base_headers(template: &OrdinaryTemplate, 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()),
);
}