ocpi_kit/server/
extract.rs1use axum::extract::{FromRequest, FromRequestParts, Query, Request};
4use axum::response::IntoResponse;
5use http::request::Parts;
6use serde::de::DeserializeOwned;
7
8use crate::transport::headers::{APPLICATION_JSON, AUTHORIZATION, header_str};
9use crate::transport::{CredentialsToken, OcpiError, PageQuery, Patch, Quirks, RequestIds, RoutingHeaders};
10use crate::types::PartyRef;
11
12use super::auth::{AuthenticatedPeer, TokenStore};
13use super::error::OcpiErrorResponse;
14
15#[derive(Clone, Debug)]
17pub struct RequestContext {
18 pub peer: AuthenticatedPeer,
20 pub ids: RequestIds,
22 pub routing: Option<RoutingHeaders>,
24}
25
26impl RequestContext {
27 #[must_use]
32 pub fn response_routing(&self, responder: &PartyRef) -> Option<RoutingHeaders> {
33 self.routing.as_ref().map(|r| r.response_from(responder.clone()))
34 }
35
36 #[must_use]
38 pub fn addressed_to(&self) -> Option<&PartyRef> {
39 self.routing.as_ref().and_then(|r| r.to.as_ref())
40 }
41
42 #[must_use]
44 pub fn from_party(&self) -> Option<&PartyRef> {
45 self.routing.as_ref().map(|r| &r.from)
46 }
47}
48
49#[derive(Clone, Debug)]
58pub struct Auth(pub AuthenticatedPeer);
59
60impl<S> FromRequestParts<S> for Auth
61where
62 S: AuthState,
63{
64 type Rejection = OcpiErrorResponse;
65
66 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
67 let ids = RequestIds::from_headers_or_generate(&parts.headers);
68 let header = header_str(&parts.headers, &AUTHORIZATION).ok_or_else(|| {
69 OcpiErrorResponse::new(OcpiError::Unauthorized("no Authorization header".to_owned()))
70 .with_ids(ids.clone())
71 })?;
72
73 let token =
74 CredentialsToken::parse_header(header, state.quirks().accept_unencoded_token).map_err(|e| {
75 OcpiErrorResponse::new(OcpiError::Unauthorized(e.to_string())).with_ids(ids.clone())
76 })?;
77
78 state.tokens().resolve(&token).map(Auth).ok_or_else(|| {
79 OcpiErrorResponse::new(OcpiError::Unauthorized(
82 "the credentials token does not match any known party".to_owned(),
83 ))
84 .with_ids(ids)
85 })
86 }
87}
88
89#[derive(Clone, Debug)]
95pub struct Ids(pub RequestIds);
96
97impl<S: Send + Sync> FromRequestParts<S> for Ids {
98 type Rejection = std::convert::Infallible;
99
100 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
101 Ok(Self(RequestIds::from_headers_or_generate(&parts.headers)))
102 }
103}
104
105#[derive(Clone, Debug)]
109pub struct Routing(pub Option<RoutingHeaders>);
110
111impl<S: Send + Sync> FromRequestParts<S> for Routing {
112 type Rejection = std::convert::Infallible;
113
114 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
115 Ok(Self(RoutingHeaders::from_headers(&parts.headers)))
116 }
117}
118
119#[derive(Clone, Debug)]
121pub struct Page(pub PageQuery);
122
123impl<S: PagePolicy> FromRequestParts<S> for Page {
124 type Rejection = OcpiErrorResponse;
125
126 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
127 let Query(query) = Query::<PageQuery>::from_request_parts(parts, state).await.map_err(|e| {
128 OcpiErrorResponse::new(OcpiError::Decode { path: "?".to_owned(), message: e.body_text() })
129 })?;
130 Ok(Self(query.clamped_to(state.max_page_limit())))
135 }
136}
137
138#[derive(Clone, Debug)]
144pub struct Owner(pub PartyRef);
145
146impl Owner {
147 pub fn from_path(country_code: &str, party_id: &str) -> Result<Self, OcpiError> {
154 PartyRef::new(country_code, party_id)
155 .map(Self)
156 .map_err(|e| OcpiError::NotFound(format!("{country_code}/{party_id}: {e}")))
157 }
158}
159
160#[derive(Clone, Debug)]
169pub struct OcpiJson<T>(pub T);
170
171impl<T, S> FromRequest<S> for OcpiJson<T>
172where
173 T: DeserializeOwned,
174 S: ContentTypePolicy,
175{
176 type Rejection = OcpiErrorResponse;
177
178 async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
179 let ids = RequestIds::from_headers_or_generate(request.headers());
180
181 if !state.accepts_content_type(request.headers()) {
182 return Err(OcpiErrorResponse::new(OcpiError::Decode {
183 path: "Content-Type".to_owned(),
184 message: format!(
185 "Content-Type SHALL be set to {APPLICATION_JSON} for any request that \
186 contains a message body"
187 ),
188 })
189 .with_ids(ids));
190 }
191
192 let bytes = axum::body::Bytes::from_request(request, state)
193 .await
194 .map_err(|e| OcpiErrorResponse::new(OcpiError::Transport(e.body_text())).with_ids(ids.clone()))?;
195
196 if serde_json::from_slice::<serde::de::IgnoredAny>(&bytes).is_err() {
198 return Err(OcpiErrorResponse::new(OcpiError::MalformedJson(
199 "the request body is not valid JSON".to_owned(),
200 ))
201 .with_ids(ids));
202 }
203
204 let mut de = serde_json::Deserializer::from_slice(&bytes);
205 serde_path_to_error::deserialize(&mut de).map(OcpiJson).map_err(|e| {
206 OcpiErrorResponse::new(OcpiError::Decode {
207 path: format!("/{}", e.path()),
208 message: e.into_inner().to_string(),
209 })
210 .with_ids(ids)
211 })
212 }
213}
214
215#[derive(Clone, Debug)]
219pub struct OcpiPatch<T>(pub Patch<T>);
220
221impl<T, S> FromRequest<S> for OcpiPatch<T>
222where
223 T: Send,
224 S: ContentTypePolicy,
225{
226 type Rejection = OcpiErrorResponse;
227
228 async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
229 let ids = RequestIds::from_headers_or_generate(request.headers());
230 let OcpiJson(value) = OcpiJson::<serde_json::Value>::from_request(request, state).await?;
231 let patch = Patch::<T>::from_value(value);
232 if patch.last_updated().is_none() {
233 return Err(OcpiErrorResponse::new(OcpiError::Decode {
234 path: "/last_updated".to_owned(),
235 message: "a PATCH must carry `last_updated`".to_owned(),
236 })
237 .with_ids(ids));
238 }
239 Ok(Self(patch))
240 }
241}
242
243pub trait AuthState: Send + Sync {
245 fn tokens(&self) -> &dyn TokenStore;
247 fn quirks(&self) -> &Quirks;
249}
250
251pub trait PagePolicy: Send + Sync {
258 fn max_page_limit(&self) -> u64;
260}
261
262pub trait ContentTypePolicy: Send + Sync {
264 fn accepts_content_type(&self, headers: &http::HeaderMap) -> bool;
266}
267
268#[must_use]
277pub fn accepts_json(headers: &http::HeaderMap, lenient: bool) -> bool {
278 let Some(value) = headers.get(http::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()) else {
279 return lenient;
280 };
281 let base = value.split(';').next().unwrap_or("").trim();
282 base.eq_ignore_ascii_case(APPLICATION_JSON) || lenient
283}
284
285#[must_use]
287pub fn reject(error: OcpiError, ids: RequestIds) -> axum::response::Response {
288 OcpiErrorResponse::new(error).with_ids(ids).into_response()
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 fn headers_with(content_type: &str) -> http::HeaderMap {
296 let mut headers = http::HeaderMap::new();
297 headers.insert(http::header::CONTENT_TYPE, http::HeaderValue::from_str(content_type).unwrap());
298 headers
299 }
300
301 #[test]
302 fn the_charset_parameter_is_accepted_but_a_wrong_type_is_not() {
303 assert!(accepts_json(&headers_with("application/json"), false));
304 assert!(accepts_json(&headers_with("application/json; charset=utf-8"), false));
305 assert!(accepts_json(&headers_with("APPLICATION/JSON"), false));
306 assert!(!accepts_json(&headers_with("text/plain"), false));
307 assert!(!accepts_json(&http::HeaderMap::new(), false));
308 }
309
310 #[test]
311 fn the_lenient_policy_accepts_anything() {
312 assert!(accepts_json(&headers_with("text/plain"), true));
313 assert!(accepts_json(&http::HeaderMap::new(), true));
314 }
315
316 #[test]
317 fn an_owner_that_is_not_a_party_reference_is_a_404() {
318 assert!(Owner::from_path("NL", "TNM").is_ok());
319 let err = Owner::from_path("TOOLONG", "TNM").unwrap_err();
320 assert_eq!(err.http_status(), 404);
321 }
322}