Skip to main content

pubhubs/api/
common.rs

1use std::rc::Rc;
2use std::str::FromStr as _;
3
4use serde::{
5    Deserialize, Serialize,
6    de::{DeserializeOwned, IntoDeserializer as _},
7};
8
9use anyhow::Context as _;
10
11use actix_web::http;
12use actix_web::http::header;
13use actix_web::web;
14
15use crate::misc::fmt_ext;
16use crate::misc::serde_ext::bytes_wrapper;
17use crate::servers::server;
18
19pub type Result<T> = std::result::Result<T, ErrorCode>;
20
21/// The [`actix_web::Responder`] used for almost all API endpoints, [`EndpointDetails`] together with an
22/// instance of [`Result<EndpointDetails::ResponseType>`].
23pub struct Responder<EP: EndpointDetails>(pub EP::ResponseType);
24
25impl<EP: EndpointDetails> actix_web::Responder for Responder<EP> {
26    type Body = <CachedResponse<EP> as actix_web::Responder>::Body;
27
28    fn respond_to(self, req: &actix_web::HttpRequest) -> actix_web::HttpResponse<Self::Body> {
29        self.into_cached().respond_to(req)
30    }
31}
32
33impl<EP: EndpointDetails> Responder<EP> {
34    pub fn into_cached(self) -> CachedResponse<EP> {
35        let payload = self.0.into_payload();
36        let mut content_type = payload.content_type();
37
38        let body = payload.into_body().unwrap_or_else(|err| {
39            log::error!(
40                "failed to serialize payload for {method} {url}: {err:#}",
41                method = EP::METHOD,
42                url = EP::PATH
43            );
44
45            content_type = Some(header::ContentType::json());
46            Some(
47                serde_json::to_vec_pretty(&Result::<()>::Err(ErrorCode::InternalError))
48                    .unwrap()
49                    .into(),
50            )
51        });
52
53        CachedResponse {
54            body,
55            content_type,
56            ep: std::marker::PhantomData,
57        }
58    }
59}
60
61/// Cached response
62#[derive(Debug)]
63pub struct CachedResponse<EP: EndpointDetails> {
64    content_type: Option<header::ContentType>,
65    body: Option<bytes::Bytes>,
66    ep: std::marker::PhantomData<EP>,
67}
68
69impl<EP: EndpointDetails> Clone for CachedResponse<EP> {
70    fn clone(&self) -> Self {
71        Self {
72            content_type: self.content_type.clone(),
73            body: self.body.clone(),
74            ep: std::marker::PhantomData::<EP>,
75        }
76    }
77}
78
79impl<EP: EndpointDetails> CachedResponse<EP> {
80    /// Returns a response builder appropriate for this response
81    pub fn response_builder(&self) -> actix_web::HttpResponseBuilder {
82        let mut rb = actix_web::HttpResponse::Ok();
83
84        if let Some(ct) = &self.content_type {
85            rb.content_type(ct.clone());
86        }
87
88        if EP::immutable_response() {
89            rb.insert_header(header::CacheControl(vec![
90                header::CacheDirective::MaxAge(i32::MAX as u32),
91                // https://github.com/actix/actix-web/issues/2666
92                header::CacheDirective::Extension("immutable".to_string(), None),
93            ]));
94        }
95
96        rb
97    }
98}
99
100impl<EP: EndpointDetails> actix_web::Responder for CachedResponse<EP> {
101    type Body = actix_web::body::BoxBody;
102
103    fn respond_to(self, _req: &actix_web::HttpRequest) -> actix_web::HttpResponse<Self::Body> {
104        let mut rb = self.response_builder();
105
106        match self.body {
107            Some(bytes) => rb.body(bytes),
108            None => rb.finish(),
109        }
110    }
111}
112
113/// Extension trait for [`std::result::Result`].
114pub trait ResultExt {
115    type Ok;
116    type Err;
117
118    /// Turns `self` into an [`ErrorCode`] result by calling `f` when `self` is an error.
119    ///
120    /// Consider logging the error you're turning into an [`ErrorCode`].
121    fn into_ec<F: FnOnce(Self::Err) -> ErrorCode>(self, f: F) -> Result<Self::Ok>;
122}
123
124impl<T, E> ResultExt for std::result::Result<T, E> {
125    type Ok = T;
126    type Err = E;
127
128    fn into_ec<F: FnOnce(E) -> ErrorCode>(self, f: F) -> Result<T> {
129        match self {
130            Ok(v) => Result::Ok(v),
131            Err(err) => Result::Err(f(err)),
132        }
133    }
134}
135
136impl<T> ResultExt for Option<T> {
137    type Ok = T;
138    type Err = ();
139
140    fn into_ec<F: FnOnce(()) -> ErrorCode>(self, f: F) -> Result<T> {
141        match self {
142            Some(v) => Result::Ok(v),
143            None => Result::Err(f(())),
144        }
145    }
146}
147
148/// Extension trait for [`Result<T,ErrorCode>`].
149pub trait ApiResultExt: Sized {
150    type Ok;
151
152    fn into_ec(self) -> Result<Self::Ok>;
153
154    /// Turns retryable errors into `None`, and the [Result] into a [std::result::Result],
155    /// making the output suitable for use with [crate::misc::task::retry].
156    fn retryable(self) -> std::result::Result<Option<Self::Ok>, ErrorCode> {
157        match self.into_ec() {
158            Ok(v) => Ok(Some(v)),
159            Err(ec) => {
160                if ec.info().retryable == Some(true) {
161                    log::trace!("ignoring retryable error: {ec}");
162                    Ok(None)
163                } else {
164                    Err(ec)
165                }
166            }
167        }
168    }
169
170    /// When passing along a result from another server to a client, this method is called
171    /// to modify any [ErrorCode]. For details, see  [ErrorCode::into_server_error].
172    fn into_server_result(self) -> Result<Self::Ok> {
173        self.into_ec().map_err(ErrorCode::into_server_error)
174    }
175}
176
177impl<T> ApiResultExt for Result<T> {
178    type Ok = T;
179
180    fn into_ec(self) -> Result<T> {
181        self
182    }
183}
184
185/// List of possible errors.  We use error codes in favour of more descriptive strings,
186/// because error codes can be more easily processed by the calling code,
187/// should change less often, and can be easily translated.
188#[derive(Clone, Copy, Serialize, Deserialize, Debug, thiserror::Error, PartialEq, Eq)]
189#[serde(deny_unknown_fields)]
190pub enum ErrorCode {
191    #[error(
192        "the request you sent failed for now, but please do retry the exact same request again"
193    )]
194    PleaseRetry,
195
196    #[error("encountered an unexpected problem")]
197    InternalError,
198
199    #[error("something is wrong with the request")]
200    BadRequest,
201}
202use ErrorCode::*;
203
204/// Information about an [ErrorCode].
205pub struct ErrorInfo {
206    /// If [true], retrying the same request might result in success.  
207    ///
208    /// If [false], the request should not be retried without relevant changes.
209    ///
210    /// If [None], we do not know.
211    pub retryable: Option<bool>,
212}
213
214impl ErrorCode {
215    /// Returns additional information about this error code.
216    pub fn info(&self) -> ErrorInfo {
217        match self {
218            PleaseRetry => ErrorInfo {
219                retryable: Some(true),
220            },
221            InternalError | BadRequest => ErrorInfo {
222                retryable: Some(false),
223            },
224        }
225    }
226
227    /// When a server receives an error from another server, it calls this method
228    /// to get the error to send tot the client.
229    pub fn into_server_error(self) -> ErrorCode {
230        match self {
231            InternalError => InternalError,
232            PleaseRetry => PleaseRetry,
233            err => {
234                if err.info().retryable == Some(true) {
235                    PleaseRetry
236                } else {
237                    InternalError
238                }
239            }
240        }
241    }
242}
243
244/// What's expected from a [`EndpointDetails::RequestType`].
245pub trait PayloadTrait: Sized {
246    type JsonType: Serialize + DeserializeOwned + core::fmt::Debug;
247
248    /// Used when creating requests
249    fn to_payload(&self) -> Payload<&Self::JsonType>;
250
251    /// Used when forming responses
252    fn into_payload(self) -> Payload<Self::JsonType>;
253
254    fn from_payload(payload: Payload<Self::JsonType>) -> anyhow::Result<Self>;
255}
256
257/// Payload of a request or response to an api endpoint.
258#[derive(Debug)]
259pub enum Payload<JsonType> {
260    None,
261
262    /// This is the regular response type used by the pubhubs API.
263    Json(JsonType),
264
265    /// Raw bytes; used, for example, by [`crate::api::phc::user::GetObjectEP`].
266    Octets(bytes::Bytes),
267}
268
269impl<T> Payload<T> {
270    /// Returns the content type appropriate for this payload
271    pub fn content_type(&self) -> Option<header::ContentType> {
272        match self {
273            Payload::None => None,
274            Payload::Json(..) => Some(header::ContentType::json()),
275            Payload::Octets(..) => Some(header::ContentType::octet_stream()),
276        }
277    }
278
279    /// Converts this payload into bytes.
280    pub fn into_body(self) -> anyhow::Result<Option<bytes::Bytes>>
281    where
282        T: Serialize,
283    {
284        match self {
285            Payload::None => Ok(None),
286            Payload::Octets(bytes) => Ok(Some(bytes)),
287            Payload::Json(tp) => Ok(Some(
288                serde_json::to_vec_pretty(&tp)
289                    .with_context(|| {
290                        format!("failed to convert {} to JSON", std::any::type_name::<T>())
291                    })?
292                    .into(),
293            )),
294        }
295    }
296
297    /// Extracts payload from [`awc::ClientResponse`].
298    pub async fn from_client_response<S>(
299        mut resp: awc::ClientResponse<S>,
300    ) -> anyhow::Result<Payload<T>>
301    where
302        S: futures::stream::Stream<
303                Item = std::result::Result<bytes::Bytes, awc::error::PayloadError>,
304            >,
305        T: DeserializeOwned,
306    {
307        let Some(content_type_hv) = resp.headers().get(http::header::CONTENT_TYPE) else {
308            anyhow::bail!("no Content-Type in response",);
309        };
310
311        let content_type = mime::Mime::from_str(
312            content_type_hv
313                .to_str()
314                .context("Content-Type value not utf8")?,
315        )
316        .context("could not parse Content-Type value")?;
317
318        match (content_type.type_(), content_type.subtype()) {
319            (mime::APPLICATION, mime::JSON) => Ok(Payload::Json({
320                let body: bytes::Bytes = resp
321                    .body()
322                    .await
323                    .context("failed to receive response body")?;
324
325                serde_json::from_slice::<T>(&body).with_context(|| {
326                    format!(
327                        "could not deserialize JSON {json} to type {tp} ",
328                        tp = std::any::type_name::<T>(),
329                        json = crate::misc::fmt_ext::Bytes(&body),
330                    )
331                })?
332            })),
333            (mime::APPLICATION, mime::OCTET_STREAM) => Ok(Payload::Octets(
334                resp.body().await.context("problem loading body")?,
335            )),
336            _ => {
337                anyhow::bail!(
338                    "expected Content-Type {} or {}, but got {content_type}",
339                    mime::APPLICATION_JSON,
340                    mime::APPLICATION_OCTET_STREAM
341                )
342            }
343        }
344    }
345}
346
347impl<T> std::fmt::Display for Payload<T>
348where
349    T: Serialize,
350{
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        match self {
353            Payload::None => write!(f, ""),
354            Payload::Json(t) => write!(f, "{}", fmt_ext::Json(t)),
355            Payload::Octets(b) => write!(f, "{} bytes", b.len()),
356        }
357    }
358}
359
360impl<T> PayloadTrait for Payload<T>
361where
362    T: Serialize + DeserializeOwned + core::fmt::Debug,
363{
364    type JsonType = T;
365
366    fn to_payload(&self) -> Payload<&T> {
367        match self {
368            Payload::None => Payload::None,
369            Payload::Json(t) => Payload::Json(t),
370            Payload::Octets(b) => Payload::Octets(b.clone()), // cheap clone
371        }
372    }
373
374    fn into_payload(self) -> Payload<T> {
375        self
376    }
377
378    fn from_payload(payload: Payload<T>) -> anyhow::Result<Self> {
379        Ok(payload)
380    }
381}
382
383impl<T> PayloadTrait for T
384where
385    T: Serialize + DeserializeOwned + core::fmt::Debug,
386{
387    type JsonType = T;
388
389    fn to_payload(&self) -> Payload<&T> {
390        Payload::Json(self)
391    }
392
393    fn into_payload(self) -> Payload<T> {
394        Payload::Json(self)
395    }
396
397    fn from_payload(payload: Payload<T>) -> anyhow::Result<T> {
398        let Payload::Json(res) = payload else {
399            anyhow::bail!("expected, but did not get, application/json");
400        };
401
402        Ok(res)
403    }
404}
405
406/// Use [`NoPayload`] as [`EndpointDetails::RequestType`] to indicate no payload is expected.
407pub struct NoPayload;
408
409impl PayloadTrait for NoPayload {
410    type JsonType = ();
411
412    fn to_payload(&self) -> Payload<&()> {
413        Payload::None
414    }
415
416    fn into_payload(self) -> Payload<()> {
417        Payload::None
418    }
419
420    fn from_payload(payload: Payload<()>) -> anyhow::Result<Self> {
421        let Payload::None = payload else {
422            anyhow::bail!("expected no payload");
423        };
424
425        Ok(NoPayload)
426    }
427}
428
429/// A payload (see [`PayloadTrait`]) that can only hold bytes. Probably only useful for as
430/// [`EndpointDetails::RequestType`].
431pub struct BytesPayload(pub bytes::Bytes);
432
433impl PayloadTrait for BytesPayload {
434    type JsonType = ();
435
436    fn to_payload(&self) -> Payload<&()> {
437        Payload::Octets(self.0.clone()) // cheap clone
438    }
439
440    fn into_payload(self) -> Payload<()> {
441        Payload::Octets(self.0)
442    }
443
444    fn from_payload(payload: Payload<()>) -> anyhow::Result<Self> {
445        let Payload::Octets(bytes) = payload else {
446            anyhow::bail!("expected, but did not get, bytes payload (application/octet-stream)");
447        };
448
449        Ok(BytesPayload(bytes))
450    }
451}
452
453/// What's expected from a [`EndpointDetails::ResponseType`].
454///
455/// Or: trait for those [`PayloadTrait`]s that have a [`PayloadTrait::JsonType`] of the form
456/// `Result<T>`, and for which `Self::from_payload(Payload::Json(Err(..)))`
457/// and `Self::from_payload(Self::into_payload())` never fail.
458pub trait ResultPayloadTrait: PayloadTrait<JsonType = Result<Self::OkType>> {
459    type OkType: Serialize + DeserializeOwned + core::fmt::Debug;
460
461    /// Create an instance of this result payload type from the given [`ErrorCode`] infallibly.
462    fn from_ec(ec: ErrorCode) -> Self {
463        Self::from_payload(Payload::Json(Err(ec))).unwrap()
464    }
465
466    /// Destructs this payload result, extracting any error.
467    fn into_result(self) -> Result<Self> {
468        match self.into_payload() {
469            Payload::Json(Err(ec)) => Err(ec),
470            oth => Ok(Self::from_payload(oth).unwrap()),
471        }
472    }
473
474    fn from_result(res: Result<Self>) -> Self {
475        res.unwrap_or_else(Self::from_ec)
476    }
477}
478
479impl<T> ResultPayloadTrait for Result<T>
480where
481    T: Serialize + DeserializeOwned + core::fmt::Debug,
482{
483    type OkType = T;
484}
485
486impl<T> ResultPayloadTrait for Payload<Result<T>>
487where
488    T: Serialize + DeserializeOwned + core::fmt::Debug,
489{
490    type OkType = T;
491}
492
493/// Details on a PubHubs server endpoint
494pub trait EndpointDetails {
495    type RequestType: PayloadTrait;
496    type ResponseType: ResultPayloadTrait;
497
498    const METHOD: http::Method;
499    const PATH: &'static str;
500
501    /// Can the response be cached indefinitely?
502    fn immutable_response() -> bool {
503        false
504    }
505
506    /// Helper function to add this endpoint to a [`web::ServiceConfig`].
507    ///
508    /// The `handler` argument must be of the form:
509    /// ```text
510    /// async fn f(app : Rc<App>, ...) -> api::ResponseType
511    /// ```
512    /// The `...` can contain arguments of type [`actix_web::FromRequest`].
513    fn add_to<App, F, Args: actix_web::FromRequest + 'static>(
514        app: &Rc<App>,
515        sc: &mut web::ServiceConfig,
516        handler: F,
517    ) where
518        server::AppMethod<App, F, Self>: actix_web::Handler<Args>,
519        <server::AppMethod<App, F, Self> as actix_web::Handler<Args>>::Output:
520            'static + actix_web::Responder,
521    {
522        sc.route(
523            Self::PATH,
524            web::method(Self::METHOD).to(server::AppMethod::new(app, handler)),
525        );
526    }
527
528    /// Like [`add_to`], but runs `handler` only once, caching the result.
529    ///
530    /// Of course, `handler` won't have access to the usual [`actix_web::FromRequest`] arguments,
531    /// as there is no request to derive these arguments from.
532    ///
533    /// Moreover `handler` cannot be `async`, since [`actix_web::App::configure`] takes a non-async
534    /// function.
535    ///
536    /// Only `application/json` responses are supported.
537    ///
538    /// # `handler` errors and panics
539    ///
540    /// If `handler` returns an [`Err`], then this will not cause the `App` (and associated `Server`) to
541    /// crash.  Instead the [`Err`] is cached and served to any client requesting this endpoint.
542    ///
543    /// If a crash is desirable, then `handler` should panic.  Unlike a panic in a regular
544    /// [`actix_web::Handler`] (which will just cause a connection reset), a panic here will cause
545    /// the `Server` to exit.
546    ///
547    /// [`Err`]: Result::Err
548    /// [`add_to`]: Self::add_to
549    fn caching_add_to<App, F>(app: &Rc<App>, sc: &mut web::ServiceConfig, handler: F)
550    where
551        F: Fn(&App) -> Self::ResponseType,
552        Self: Sized + 'static,
553    {
554        let cached = Responder::<Self>(handler(app)).into_cached();
555
556        // TODO: etag
557        sc.route(
558            Self::PATH,
559            web::method(Self::METHOD).to(move || {
560                let cached = cached.clone(); // cheap clone
561                async { cached }
562            }),
563        );
564    }
565}
566
567/// Wraps one of the dalek types to enforce hex serialization
568macro_rules! wrap_dalek_type {
569    {$type:ident, $wrapped_type:path, derive( $($derive:tt)* ), $visitor_type:path } => {
570        #[doc = "Wrapper around [`"]
571        #[doc = stringify!($wrapped_type)]
572        #[doc = "`] enforcing base16 serialization."]
573        #[derive(Clone, Debug, Serialize, Deserialize, $( $derive )* )]
574        #[serde(transparent)]
575        pub struct $type {
576            inner: bytes_wrapper::BytesWrapper<
577                $wrapped_type,
578                bytes_wrapper::ChangeVisitorType<
579                    (bytes_wrapper::B16Encoding,),
580                    { $visitor_type as isize },
581                >,
582            >,
583        }
584
585        impl From<$wrapped_type> for $type {
586            fn from(inner: $wrapped_type) -> Self {
587                Self {
588                    inner: inner.into(),
589                }
590            }
591        }
592
593        /// We implement [`std::str::FromStr`] so that this type can be used as a command-line argument with [`clap`].
594        impl std::str::FromStr for $type {
595            type Err = serde::de::value::Error;
596
597            fn from_str(s : &str) -> std::result::Result<Self, Self::Err> {
598                Self::deserialize(s.into_deserializer())
599            }
600        }
601
602        impl core::ops::Deref for $type {
603            type Target = $wrapped_type;
604
605            fn deref(&self) -> &Self::Target {
606                &self.inner
607            }
608        }
609
610        impl core::ops::DerefMut for $type {
611            fn deref_mut(&mut self) -> &mut Self::Target {
612                &mut self.inner
613            }
614        }
615
616        impl std::fmt::Display for $type {
617            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618                self.serialize(f)
619            }
620        }
621    }
622}
623
624wrap_dalek_type! {
625    VerifyingKey, ed25519_dalek::VerifyingKey,
626    derive(PartialEq, Eq),
627    bytes_wrapper::VisitorType::BorrowedByteArray
628}
629
630wrap_dalek_type! {
631    SigningKey, ed25519_dalek::SigningKey,
632    derive(),
633    bytes_wrapper::VisitorType::BorrowedByteArray
634}
635
636wrap_dalek_type! {
637    Scalar, curve25519_dalek::scalar::Scalar,
638    derive(PartialEq, Eq),
639    bytes_wrapper::VisitorType::ByteSequence
640}
641
642wrap_dalek_type! {
643    CurvePoint, curve25519_dalek::ristretto::CompressedRistretto,
644    derive(PartialEq, Eq),
645    bytes_wrapper::VisitorType::ByteSequence
646}
647
648impl SigningKey {
649    pub fn generate() -> Self {
650        ed25519_dalek::SigningKey::generate(&mut aead::rand_core::OsRng).into()
651    }
652}
653
654impl Scalar {
655    pub fn random() -> Self {
656        curve25519_dalek::scalar::Scalar::random(&mut aead::rand_core::OsRng).into()
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663
664    #[test]
665    fn serde_scalar() {
666        assert_eq!(
667            Scalar::deserialize(
668                serde::de::value::StrDeserializer::<serde::de::value::Error>::new(
669                    &"ff00000000000000000000000000000000000000000000000000000000000000",
670                ),
671            )
672            .unwrap(),
673            curve25519_dalek::scalar::Scalar::from(255u8).into(),
674        );
675
676        let s: Scalar = curve25519_dalek::scalar::Scalar::from(1u8).into();
677        assert_eq!(
678            &serde_json::to_string(&s).unwrap(),
679            "\"0100000000000000000000000000000000000000000000000000000000000000\""
680        );
681    }
682}