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 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 fn verify(&self, delivery: &Delivery<'_>) -> Result<String>;
62}
63
64pub 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 #[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
120pub(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}