Skip to main content

sofie_http/
webhook.rs

1use std::sync::Arc;
2
3use bytes::Bytes;
4use http::{HeaderMap, Method, StatusCode};
5use sofie_auth::digest;
6use sofie_core::{Result, Timestamp};
7use sofie_db::{Call, Work};
8
9use crate::admission::Visit;
10use crate::answer::{self, Response};
11use crate::handler::{Endpoint, endpoint};
12use crate::routes::{RouteRules, View};
13use crate::{RequestContext, Routes, Web};
14
15const UNVERIFIED: &str = "The delivery could not be verified.";
16
17#[must_use = "a webhook is served only once `.verify(…)` names how its deliveries are verified"]
18pub struct WebhookRoute<'r> {
19    routes: &'r mut Routes<Web>,
20    path: String,
21    endpoint: Endpoint,
22    body_limit: Option<usize>,
23}
24
25impl Routes<Web> {
26    /// Opens no session, needs no same-origin proof; each delivery id is answered once.
27    pub fn webhook<Args: 'static, H>(&mut self, path: &str, handler: H) -> WebhookRoute<'_>
28    where
29        H: Call<RequestContext<Web>, Args, Outcome: Send>,
30    {
31        WebhookRoute {
32            path: path.to_owned(),
33            endpoint: endpoint(handler),
34            body_limit: None,
35            routes: self,
36        }
37    }
38}
39
40impl WebhookRoute<'_> {
41    pub fn body_limit(self, bytes: usize) -> Self {
42        Self {
43            body_limit: Some(bytes),
44            ..self
45        }
46    }
47
48    pub fn verify(self, verifier: impl Verify) {
49        let rules = RouteRules {
50            body_limit: self.body_limit,
51            verify: Some(Arc::new(verifier)),
52            ..RouteRules::default()
53        };
54        self.routes
55            .push(Method::POST, &self.path, self.endpoint, View::Json, rules);
56    }
57}
58
59pub trait Verify: Send + Sync + 'static {
60    /// The id must not change between attempts: a redelivery answers as the first did.
61    fn verify(&self, delivery: &Delivery<'_>) -> Result<String>;
62}
63
64/// The body exactly as sent.
65pub struct Delivery<'r> {
66    headers: &'r HeaderMap,
67    body: &'r [u8],
68    now: Timestamp,
69}
70
71impl Delivery<'_> {
72    #[must_use]
73    pub fn header(&self, name: &str) -> Option<&str> {
74        self.headers.get(name)?.to_str().ok()
75    }
76
77    #[must_use]
78    pub fn body(&self) -> &[u8] {
79        self.body
80    }
81
82    /// For checking a signed timestamp against replays.
83    #[must_use]
84    pub fn now(&self) -> Timestamp {
85        self.now
86    }
87}
88
89pub(crate) async fn deliver(
90    pipeline: &crate::pipeline::Pipeline,
91    asked: crate::pipeline::Asked<'_>,
92    target: &crate::target::Target,
93    parameters: Vec<(String, String)>,
94    body: &mut Option<hyper::body::Incoming>,
95) -> std::result::Result<Response, Box<Response>> {
96    #[cfg(feature = "shield")]
97    if let Some(refusal) = pipeline.shield.refusal(asked, None) {
98        return Err(Box::new(refusal));
99    }
100    let now = pipeline.db.clock().now().timestamp();
101    if let Some(refusal) =
102        target
103            .admission
104            .refusal(Visit::new(asked, None, now, &pipeline.sessions))
105    {
106        return Err(Box::new(refusal));
107    }
108    let found = crate::pipeline::Found {
109        target,
110        parameters,
111        session: crate::session::RequestSession::default(),
112        reach: sofie_db::Reach::All,
113        now,
114        #[cfg(feature = "gates")]
115        gates: crate::gate::RequestGates::default(),
116    };
117    pipeline.guarded(asked, found, body).await
118}
119
120/// A resend may carry other headers or a fresh signature, so the verified id alone decides.
121pub(crate) fn delivered_work(
122    verify: &dyn Verify,
123    visit: Visit<'_>,
124    pattern: &str,
125    body: &Bytes,
126) -> Result<Work, Box<Response>> {
127    let asked = visit.asked;
128    let delivery = Delivery {
129        headers: &asked.request.headers,
130        body,
131        now: visit.now,
132    };
133    match verify.verify(&delivery) {
134        Ok(id) => Ok(Work::new(
135            digest(&[b"delivery", pattern.as_bytes(), id.as_bytes()]),
136            String::new(),
137        )),
138        Err(error) => {
139            tracing::warn!(
140                http.route = pattern,
141                error.kind = "webhook",
142                error = %error,
143                "refused a webhook delivery"
144            );
145            Err(Box::new(answer::refusal(
146                StatusCode::UNAUTHORIZED,
147                UNVERIFIED,
148                asked.caller,
149            )))
150        }
151    }
152}