use super::Alibi;
use super::endpoint::merge_headers;
use alibi_core::endpoint::{
BeforeEndpointAction, EndpointCall, EndpointContextPatch, EndpointResponse,
is_endpoint_api_error, with_endpoint_call_context,
};
use alibi_core::{AuthError, AuthSchema, Headers};
use std::sync::Arc;
type HttpEndpointError = (u16, Option<String>, String);
#[derive(Clone)]
pub(super) struct HttpEndpointFrame {
pub call: EndpointCall,
pub outer: EndpointCall,
pub path_patch: Option<String>,
pub header_patch: Option<std::collections::HashMap<String, String>>,
pub legacy_headers: Option<std::collections::HashMap<String, String>>,
pub error: Arc<std::sync::Mutex<Option<HttpEndpointError>>>,
pub returned_response: Arc<std::sync::Mutex<Option<alibi_core::AuthResponse>>>,
}
pub(super) fn with_http_endpoint_call_context<T>(
call: EndpointCall,
future: impl Future<Output = T>,
) -> impl Future<Output = T> {
let request = alibi_core::hooks::current_request_hook_context();
Box::pin(with_endpoint_call_context(
call,
alibi_core::hooks::with_optional_request_hook_context(request, future),
))
}
impl<S: AuthSchema> Alibi<S> {
pub(super) async fn before_http_endpoint(
&self,
request: &mut alibi_core::AuthRequest,
route: &alibi_core::AuthRoute,
context: &alibi_core::AuthContext<S>,
) -> alibi_core::AuthResult<Option<alibi_core::AuthResponse>> {
let mut call = EndpointCall::from_http_request(request, route);
call.extensions().insert((*context.config).clone());
let mut outer = call.clone();
let mut patch = EndpointContextPatch::default();
let mut headers = Headers::new();
for hook in &self.endpoint_hooks {
let matches = with_http_endpoint_call_context(call.clone(), async { hook.matches_before(&call, context) }).await.map_err(|error| {
tracing::error!(%error, "Endpoint before-hook matcher failed");
AuthError::Api { status: 500, code: None, message: "An error occurred during hook matcher execution. Check the logs for more details.".into() }
})?;
if !matches {
continue;
}
let middleware = call.middleware_context();
let action =
with_http_endpoint_call_context(call.clone(), hook.before(&middleware, context))
.await;
let emitted = call.take_response_headers();
let action = match action {
Ok(action) => action,
Err(error) if is_endpoint_api_error(&error) => {
let mut response = error.to_auth_response();
merge_headers(&mut response.headers, emitted);
return Ok(Some(response));
}
Err(error) => return Err(ordinary_http_hook_error(error)),
};
let hook_headers = emitted.clone();
merge_headers(&mut headers, emitted);
match action {
Some(BeforeEndpointAction::Patch(changes)) => patch.merge(*changes),
Some(BeforeEndpointAction::Respond(mut response)) => {
response.merge_headers(headers);
return render_http_endpoint_response(&response).map(Some);
}
Some(BeforeEndpointAction::Reject(mut response)) => {
response.merge_headers(hook_headers);
return render_http_endpoint_response(&response).map(Some);
}
None => {}
}
}
let path_patch = patch.path.clone();
let header_patch = patch.headers.clone();
patch.apply(&mut call);
EndpointContextPatch {
headers: call.headers().cloned(),
..Default::default()
}
.apply(&mut outer);
for (name, value) in headers {
request.queue_response_header(name, value);
}
request.extensions().insert(HttpEndpointFrame {
call,
outer,
path_patch,
header_patch,
legacy_headers: None,
error: Arc::default(),
returned_response: Arc::default(),
});
Ok(None)
}
pub(super) async fn after_http_endpoint(
&self,
request: &alibi_core::AuthRequest,
context: &alibi_core::AuthContext<S>,
mut response: alibi_core::AuthResponse,
) -> alibi_core::AuthResult<alibi_core::AuthResponse> {
let Some(frame) = request.extensions().get::<HttpEndpointFrame>() else {
return Ok(response);
};
if let Some((user, session)) = request.session_hook_snapshot() {
frame
.call
.record_authenticated_session(user.clone(), session.clone());
frame.call.set_session_hook_snapshot(user, session);
}
let original =
alibi_core::utils::json::from_slice::<alibi_core::utils::json::JsValue>(&response.body)
.unwrap_or(alibi_core::utils::json::JsValue::Null);
let returned_response = frame
.returned_response
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let mut logical = if let Some(raw) = returned_response.as_ref() {
EndpointResponse::from_http_response(raw.clone())
} else if let Some((status, code, message)) = frame
.error
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
{
EndpointResponse::error(AuthError::Api {
status,
code,
message,
})
.with_error_body(original.clone())
} else {
EndpointResponse::value(original.clone()).with_status(response.status)
};
let headers = std::mem::take(&mut response.headers);
if let Some(raw) = returned_response.as_ref() {
let mut metadata = Headers::new();
for (name, value) in headers {
if !raw.headers.iter().any(|(raw_name, raw_value)| {
raw_name.eq_ignore_ascii_case(&name) && raw_value == &value
}) {
metadata.append(name, value);
}
}
logical.merge_headers(metadata);
} else {
logical.merge_headers(headers);
}
for hook in &self.endpoint_hooks {
if !with_http_endpoint_call_context(frame.outer.clone(), async {
hook.matches_after(&frame.call, context, &logical)
})
.await
.map_err(|error| {
if is_endpoint_api_error(&error) {
error
} else {
ordinary_http_hook_error(error)
}
})? {
continue;
}
let middleware = frame.call.middleware_context();
let prior_headers = logical.headers().clone();
let status = logical.status();
logical = match with_http_endpoint_call_context(
frame.outer.clone(),
hook.after(&middleware, context, logical),
)
.await
{
Ok(response) => response,
Err(error) if is_endpoint_api_error(&error) => {
let mut response = EndpointResponse::error(error);
response.set_status(status);
response.merge_headers(prior_headers);
response
}
Err(error) => return Err(ordinary_http_hook_error(error)),
};
logical.merge_headers(frame.call.take_response_headers());
}
alibi_core::session::cookie_cache::runtime::retain_publication(&frame.call, request);
if matches!(logical.result(), Ok(value) if value == &original) {
response.status = logical.status().unwrap_or(response.status);
response.headers = logical.headers().clone();
Ok(response)
} else {
render_http_endpoint_response(&logical)
}
}
}
pub(super) fn apply_http_endpoint_input(
request: &mut alibi_core::AuthRequest,
) -> alibi_core::AuthResult<()> {
let Some(frame) = request.extensions().get::<HttpEndpointFrame>() else {
return Ok(());
};
let mut frame = (*frame).clone();
if let Some(headers) = &frame.header_patch {
request.headers.extend(headers.clone());
}
if let Some(headers) = &frame.legacy_headers {
request.headers.extend(headers.clone());
}
frame.call.replace_headers(request.headers.clone());
EndpointContextPatch {
headers: Some(request.headers.clone()),
..Default::default()
}
.apply(&mut frame.outer);
let call = &frame.call;
if let Some(path) = &frame.path_patch {
path.clone_into(&mut request.path);
}
if let Some(method) = call.method() {
request.method = method.clone();
}
if let Some(body) = call.body() {
request.body = Some(alibi_core::utils::json::to_vec(body)?);
request
.extensions()
.insert(alibi_core::types::ParsedRequestBody::Value(body.clone()));
}
if let Some(alibi_core::utils::json::JsValue::Object(query)) = call.query() {
request.set_query_pairs(query.iter().flat_map(|(name, value)| {
let values = match value {
alibi_core::utils::json::JsValue::Array(values) => values.clone(),
value => vec![value.clone()],
};
values.into_iter().map(|value| {
(
name.clone(),
match value {
alibi_core::utils::json::JsValue::String(value) => value,
value => alibi_core::utils::json::to_string(&value).unwrap_or_default(),
},
)
})
}));
}
request.extensions().insert(frame);
Ok(())
}
fn ordinary_http_hook_error(error: AuthError) -> AuthError {
match error {
AuthError::CallbackFailure(_) => error,
error => AuthError::CallbackFailure(Box::new(error)),
}
}
fn render_http_endpoint_response(
response: &EndpointResponse,
) -> alibi_core::AuthResult<alibi_core::AuthResponse> {
if response.result().is_ok() {
return response.to_response();
}
let mut rendered = match response.result() {
Ok(value) => alibi_core::AuthResponse::json(response.status().unwrap_or(200), value)?,
Err(error) => {
let (status, code, message) = error.error_payload();
let mut rendered = alibi_core::AuthResponse::json(
status,
&alibi_core::ErrorCodeMessageResponse { code, message },
)?;
if response.error_body_is_absent() {
rendered.body.clear();
} else if let Some(body) = response.error_body() {
rendered.body = alibi_core::utils::json::to_vec(body)?;
}
rendered
}
};
merge_headers(&mut rendered.headers, response.headers().clone());
Ok(rendered)
}