Skip to main content

ocpi_kit/server/
extract.rs

1//! Axum extractors for the things every OCPI request carries.
2
3use 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/// The pieces of a request a handler is given.
16#[derive(Clone, Debug)]
17pub struct RequestContext {
18    /// The platform the request was authenticated as.
19    pub peer: AuthenticatedPeer,
20    /// The request and correlation IDs, to be echoed on the response.
21    pub ids: RequestIds,
22    /// The routing headers, when the module is a functional one and the peer sent them.
23    pub routing: Option<RoutingHeaders>,
24}
25
26impl RequestContext {
27    /// The routing headers for the response to this request.
28    ///
29    /// > *Direct response | Receiving platform provider to Requesting platform provider |
30    /// > Requesting-party | Receiving-party*
31    #[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    /// The party the request was addressed to, if the peer said.
37    #[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    /// The party the request came from, if the peer said.
43    #[must_use]
44    pub fn from_party(&self) -> Option<&PartyRef> {
45        self.routing.as_ref().map(|r| &r.from)
46    }
47}
48
49/// Extracts and authenticates the credentials token.
50///
51/// > *If the header is missing or the credentials token doesn't match any known party then the
52/// > server SHALL respond with an HTTP `401 - Unauthorized` status code.*
53///
54/// The router provides the [`TokenStore`] through axum state.
55///
56/// Spec: 2.3.0 §transport_and_format_authorization_header
57#[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            // Deliberately the same message as a missing header: it says nothing about whether
80            // the token merely expired or never existed.
81            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/// The `X-Request-ID` and `X-Correlation-ID` of the request.
90///
91/// Missing IDs are generated rather than refused: they are required of the peer, but failing a
92/// request over a missing debugging header would be worse than answering it and echoing what was
93/// used.
94#[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/// The `OCPI-to-*` and `OCPI-from-*` headers, when present.
106///
107/// Absent on a configuration module, and on the request half of an Open Routing Request.
108#[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/// The pagination query parameters.
120#[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        // The handler is given a limit it can honour literally. A peer asking for `?limit=100000`
131        // gets the server's own maximum instead, which is the number `X-Limit` on the way back
132        // says it would get — a cap that is only advertised is not a cap, and the alternative is
133        // that every handler has to remember to clamp.
134        Ok(Self(query.clamped_to(state.max_page_limit())))
135    }
136}
137
138/// The `{country_code}/{party_id}` pair of a client-owned-object URL.
139///
140/// Extract this together with [`Auth`] and call
141/// [`AuthenticatedPeer::check_ownership`](super::auth::AuthenticatedPeer::check_ownership): a
142/// platform may only write under its own party.
143#[derive(Clone, Debug)]
144pub struct Owner(pub PartyRef);
145
146impl Owner {
147    /// Builds an owner from the two path segments.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`OcpiError::NotFound`] when the segments are not a usable party reference, which
152    /// is the same answer as "no such object" and reveals nothing.
153    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/// A JSON body, decoded with the path to any offending value.
161///
162/// A body that is not valid JSON is an HTTP 400; a body that is valid JSON but not the object the
163/// endpoint expects is a `2001` in a 200. That is exactly the line the specification draws:
164///
165/// > *The transport layer ends after a message is correctly parsed into a (semantically
166/// > unvalidated) JSON structure. When a message does not contain a valid JSON string, the HTTP
167/// > error `400 - Bad request` MUST be returned.*
168#[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        // Distinguish "not JSON at all" (HTTP 400) from "not this object" (2001 in a 200).
197        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/// A JSON Merge Patch body.
216///
217/// Refuses a patch with no `last_updated`, which is the specification's own example of a `2001`.
218#[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
243/// What [`Auth`] needs from the router's state.
244pub trait AuthState: Send + Sync {
245    /// The token store to resolve credentials tokens against.
246    fn tokens(&self) -> &dyn TokenStore;
247    /// The interoperability profile to parse the `Authorization` header with.
248    fn quirks(&self) -> &Quirks;
249}
250
251/// What [`Page`] needs from the router's state.
252///
253/// > *`X-Limit`: The maximum number of objects that the server can return.*
254///
255/// The header is a promise, so the extractor keeps it: a `limit` above this never reaches a
256/// handler.
257pub trait PagePolicy: Send + Sync {
258    /// The largest page this server will return.
259    fn max_page_limit(&self) -> u64;
260}
261
262/// What [`OcpiJson`] needs from the router's state.
263pub trait ContentTypePolicy: Send + Sync {
264    /// Whether a request with these headers carries an acceptable `Content-Type`.
265    fn accepts_content_type(&self, headers: &http::HeaderMap) -> bool;
266}
267
268/// The default reading of the `Content-Type` rule.
269///
270/// > *The HTTP header: Content-Type SHALL be set to `application/json` for any request that
271/// > contains a message body.*
272///
273/// `application/json; charset=utf-8` is accepted, because it says the same thing and is extremely
274/// common. `lenient` additionally accepts an absent or unrelated type, for a peer that cannot be
275/// persuaded to set it.
276#[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/// Renders a rejection, so a handler can return one directly.
286#[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}