use axum::extract::{FromRequest, FromRequestParts, Query, Request};
use axum::response::IntoResponse;
use http::request::Parts;
use serde::de::DeserializeOwned;
use crate::transport::headers::{APPLICATION_JSON, AUTHORIZATION, header_str};
use crate::transport::{CredentialsToken, OcpiError, PageQuery, Patch, Quirks, RequestIds, RoutingHeaders};
use crate::types::PartyRef;
use super::auth::{AuthenticatedPeer, TokenStore};
use super::error::OcpiErrorResponse;
#[derive(Clone, Debug)]
pub struct RequestContext {
pub peer: AuthenticatedPeer,
pub ids: RequestIds,
pub routing: Option<RoutingHeaders>,
}
impl RequestContext {
#[must_use]
pub fn response_routing(&self, responder: &PartyRef) -> Option<RoutingHeaders> {
self.routing.as_ref().map(|r| r.response_from(responder.clone()))
}
#[must_use]
pub fn addressed_to(&self) -> Option<&PartyRef> {
self.routing.as_ref().and_then(|r| r.to.as_ref())
}
#[must_use]
pub fn from_party(&self) -> Option<&PartyRef> {
self.routing.as_ref().map(|r| &r.from)
}
}
#[derive(Clone, Debug)]
pub struct Auth(pub AuthenticatedPeer);
impl<S> FromRequestParts<S> for Auth
where
S: AuthState,
{
type Rejection = OcpiErrorResponse;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let ids = RequestIds::from_headers_or_generate(&parts.headers);
let header = header_str(&parts.headers, &AUTHORIZATION).ok_or_else(|| {
OcpiErrorResponse::new(OcpiError::Unauthorized("no Authorization header".to_owned()))
.with_ids(ids.clone())
})?;
let token =
CredentialsToken::parse_header(header, state.quirks().accept_unencoded_token).map_err(|e| {
OcpiErrorResponse::new(OcpiError::Unauthorized(e.to_string())).with_ids(ids.clone())
})?;
state.tokens().resolve(&token).map(Auth).ok_or_else(|| {
OcpiErrorResponse::new(OcpiError::Unauthorized(
"the credentials token does not match any known party".to_owned(),
))
.with_ids(ids)
})
}
}
#[derive(Clone, Debug)]
pub struct Ids(pub RequestIds);
impl<S: Send + Sync> FromRequestParts<S> for Ids {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self(RequestIds::from_headers_or_generate(&parts.headers)))
}
}
#[derive(Clone, Debug)]
pub struct Routing(pub Option<RoutingHeaders>);
impl<S: Send + Sync> FromRequestParts<S> for Routing {
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self(RoutingHeaders::from_headers(&parts.headers)))
}
}
#[derive(Clone, Debug)]
pub struct Page(pub PageQuery);
impl<S: PagePolicy> FromRequestParts<S> for Page {
type Rejection = OcpiErrorResponse;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let Query(query) = Query::<PageQuery>::from_request_parts(parts, state).await.map_err(|e| {
OcpiErrorResponse::new(OcpiError::Decode { path: "?".to_owned(), message: e.body_text() })
})?;
Ok(Self(query.clamped_to(state.max_page_limit())))
}
}
#[derive(Clone, Debug)]
pub struct Owner(pub PartyRef);
impl Owner {
pub fn from_path(country_code: &str, party_id: &str) -> Result<Self, OcpiError> {
PartyRef::new(country_code, party_id)
.map(Self)
.map_err(|e| OcpiError::NotFound(format!("{country_code}/{party_id}: {e}")))
}
}
#[derive(Clone, Debug)]
pub struct OcpiJson<T>(pub T);
impl<T, S> FromRequest<S> for OcpiJson<T>
where
T: DeserializeOwned,
S: ContentTypePolicy,
{
type Rejection = OcpiErrorResponse;
async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
let ids = RequestIds::from_headers_or_generate(request.headers());
if !state.accepts_content_type(request.headers()) {
return Err(OcpiErrorResponse::new(OcpiError::Decode {
path: "Content-Type".to_owned(),
message: format!(
"Content-Type SHALL be set to {APPLICATION_JSON} for any request that \
contains a message body"
),
})
.with_ids(ids));
}
let bytes = axum::body::Bytes::from_request(request, state)
.await
.map_err(|e| OcpiErrorResponse::new(OcpiError::Transport(e.body_text())).with_ids(ids.clone()))?;
if serde_json::from_slice::<serde::de::IgnoredAny>(&bytes).is_err() {
return Err(OcpiErrorResponse::new(OcpiError::MalformedJson(
"the request body is not valid JSON".to_owned(),
))
.with_ids(ids));
}
let mut de = serde_json::Deserializer::from_slice(&bytes);
serde_path_to_error::deserialize(&mut de).map(OcpiJson).map_err(|e| {
OcpiErrorResponse::new(OcpiError::Decode {
path: format!("/{}", e.path()),
message: e.into_inner().to_string(),
})
.with_ids(ids)
})
}
}
#[derive(Clone, Debug)]
pub struct OcpiPatch<T>(pub Patch<T>);
impl<T, S> FromRequest<S> for OcpiPatch<T>
where
T: Send,
S: ContentTypePolicy,
{
type Rejection = OcpiErrorResponse;
async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
let ids = RequestIds::from_headers_or_generate(request.headers());
let OcpiJson(value) = OcpiJson::<serde_json::Value>::from_request(request, state).await?;
let patch = Patch::<T>::from_value(value);
if patch.last_updated().is_none() {
return Err(OcpiErrorResponse::new(OcpiError::Decode {
path: "/last_updated".to_owned(),
message: "a PATCH must carry `last_updated`".to_owned(),
})
.with_ids(ids));
}
Ok(Self(patch))
}
}
pub trait AuthState: Send + Sync {
fn tokens(&self) -> &dyn TokenStore;
fn quirks(&self) -> &Quirks;
}
pub trait PagePolicy: Send + Sync {
fn max_page_limit(&self) -> u64;
}
pub trait ContentTypePolicy: Send + Sync {
fn accepts_content_type(&self, headers: &http::HeaderMap) -> bool;
}
#[must_use]
pub fn accepts_json(headers: &http::HeaderMap, lenient: bool) -> bool {
let Some(value) = headers.get(http::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()) else {
return lenient;
};
let base = value.split(';').next().unwrap_or("").trim();
base.eq_ignore_ascii_case(APPLICATION_JSON) || lenient
}
#[must_use]
pub fn reject(error: OcpiError, ids: RequestIds) -> axum::response::Response {
OcpiErrorResponse::new(error).with_ids(ids).into_response()
}
#[cfg(test)]
mod tests {
use super::*;
fn headers_with(content_type: &str) -> http::HeaderMap {
let mut headers = http::HeaderMap::new();
headers.insert(http::header::CONTENT_TYPE, http::HeaderValue::from_str(content_type).unwrap());
headers
}
#[test]
fn the_charset_parameter_is_accepted_but_a_wrong_type_is_not() {
assert!(accepts_json(&headers_with("application/json"), false));
assert!(accepts_json(&headers_with("application/json; charset=utf-8"), false));
assert!(accepts_json(&headers_with("APPLICATION/JSON"), false));
assert!(!accepts_json(&headers_with("text/plain"), false));
assert!(!accepts_json(&http::HeaderMap::new(), false));
}
#[test]
fn the_lenient_policy_accepts_anything() {
assert!(accepts_json(&headers_with("text/plain"), true));
assert!(accepts_json(&http::HeaderMap::new(), true));
}
#[test]
fn an_owner_that_is_not_a_party_reference_is_a_404() {
assert!(Owner::from_path("NL", "TNM").is_ok());
let err = Owner::from_path("TOOLONG", "TNM").unwrap_err();
assert_eq!(err.http_status(), 404);
}
}