1use std::collections::HashMap;
5use std::ops::ControlFlow;
6use std::sync::Arc;
7
8use http::{Method, StatusCode};
9use hyper::body::Incoming;
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use serde_json::Value;
13use sofie_auth::{Acting, SigningKeys};
14use sofie_core::{Error, Result, Uuid};
15use sofie_db::remote::{CONTRACTS_PATH, FEED_PATH, LOOKUPS_PATH, PRINCIPAL_HEADER, lookup_name};
16use sofie_db::{BoxFuture, Db, Lookup};
17
18use crate::answer::{self, JSON, Response};
19use crate::caller::Caller;
20use crate::pipeline::{Asked, read_body};
21use crate::routes::BODY_LIMIT;
22
23const UNVERIFIED: &str = "The call between apps could not be verified.";
24
25pub trait Serve {
27 const CONTRACT: &'static str;
29
30 fn serve<'a>(&'a self, method: &'a str, arguments: Value) -> BoxFuture<'a, Result<Value>>;
31}
32
33pub fn arguments<T: DeserializeOwned>(arguments: Value) -> Result<T> {
34 serde_json::from_value(arguments).map_err(|error| {
35 Error::BadRequest(format!(
36 "the arguments of a contract call do not fit: {error}"
37 ))
38 })
39}
40
41pub fn encoded<T: Serialize>(arguments: &T) -> Result<Value> {
42 serde_json::to_value(arguments)
43 .map_err(|error| Error::internal("serializing a contract call's arguments", error))
44}
45
46pub fn answered<T: Serialize>(answer: &T) -> Result<Value> {
47 serde_json::to_value(answer)
48 .map_err(|error| Error::internal("serializing a contract's answer", error))
49}
50
51#[must_use]
52pub fn unknown_method(contract: &str, method: &str) -> Error {
53 Error::BadRequest(format!("`{contract}` has no method `{method}`"))
54}
55
56type Dispatch = Arc<dyn Fn(&Db, &str, Value) -> BoxFuture<'static, Result<Value>> + Send + Sync>;
57
58#[derive(Default)]
59pub struct Served {
60 contracts: HashMap<&'static str, Dispatch>,
61 lookups: HashMap<&'static str, Dispatch>,
62 feed: bool,
63}
64
65pub(crate) async fn answer(
67 asked: Asked<'_>,
68 db: &Db,
69 keys: &SigningKeys,
70 body: &mut Incoming,
71) -> ControlFlow<Response> {
72 if !asked.request.uri.path().starts_with("/_sofie/") {
73 return ControlFlow::Continue(());
74 }
75 match sofie_db::provided::<Served>(db) {
76 Some(served) => served.answer(asked, db, keys, body).await,
77 None => ControlFlow::Continue(()),
78 }
79}
80
81impl Served {
82 #[must_use]
83 pub fn contract<T: ?Sized + Serve + Send + Sync + 'static>(mut self) -> Self {
84 let dispatch: Dispatch = Arc::new(|db, method, arguments| {
85 let implementation = sofie_db::provided::<T>(db);
86 let method = method.to_owned();
87 Box::pin(async move {
88 let implementation = implementation.ok_or_else(|| unprovided::<T>())?;
89 implementation.serve(&method, arguments).await
90 })
91 });
92 self.contracts.insert(T::CONTRACT, dispatch);
93 self
94 }
95
96 #[must_use]
97 pub fn lookup<D: Serialize + Send + 'static>(mut self) -> Self {
98 let dispatch: Dispatch = Arc::new(|db, _, arguments| {
99 let lookup = sofie_db::provided::<dyn Lookup<D>>(db);
100 Box::pin(async move {
101 let lookup = lookup.ok_or_else(unprovided::<dyn Lookup<D>>)?;
102 let ids: Vec<Uuid> = self::arguments(arguments)?;
103 answered(&lookup.get_many(&ids).await?)
104 })
105 });
106 self.lookups.insert(lookup_name::<D>(), dispatch);
107 self
108 }
109
110 #[must_use]
111 pub fn feed(mut self) -> Self {
112 self.feed = true;
113 self
114 }
115
116 pub fn serve(self, db: &Db) {
118 sofie_db::provide::<Self>(db, Arc::new(self));
119 }
120
121 async fn answer(
122 &self,
123 asked: Asked<'_>,
124 db: &Db,
125 keys: &SigningKeys,
126 body: &mut Incoming,
127 ) -> ControlFlow<Response> {
128 let request = asked.request;
129 let path = request.uri.path();
130 let call = if let Some(rest) = path.strip_prefix(CONTRACTS_PATH) {
131 rest.rsplit_once('/')
132 .and_then(|(contract, method)| Some((self.contracts.get(contract)?, method)))
133 } else if let Some(name) = path.strip_prefix(LOOKUPS_PATH) {
134 self.lookups.get(name).map(|dispatch| (dispatch, ""))
135 } else if path == FEED_PATH && self.feed && request.method == Method::GET {
136 let feed = verified(asked, db, keys).map(|_| feed(db, request.uri.query()));
137 return ControlFlow::Break(match feed {
138 Ok(feed) => json(feed.await),
139 Err(refused) => *refused,
140 });
141 } else {
142 return ControlFlow::Continue(());
143 };
144 let Some((dispatch, method)) = call.filter(|_| request.method == Method::POST) else {
145 return ControlFlow::Break(answer::failure(Error::NotFound, Caller::Json));
146 };
147 let acting = match verified(asked, db, keys) {
148 Ok(acting) => acting,
149 Err(refused) => return ControlFlow::Break(*refused),
150 };
151 let body = match read_body(body, BODY_LIMIT, Caller::Json).await {
152 Ok(body) => body,
153 Err(refused) => return ControlFlow::Break(*refused),
154 };
155 let called = async {
156 let arguments = serde_json::from_slice(&body).map_err(|error| {
157 Error::BadRequest(format!("a contract call's body is not JSON: {error}"))
158 })?;
159 sofie_db::remote::acting_as(acting, dispatch(db, method, arguments)).await
160 };
161 ControlFlow::Break(json(called.await))
162 }
163}
164
165fn verified(
166 asked: Asked<'_>,
167 db: &Db,
168 keys: &SigningKeys,
169) -> std::result::Result<Option<Acting>, Box<Response>> {
170 let request = asked.request;
171 let token = request
172 .headers
173 .get(PRINCIPAL_HEADER)
174 .and_then(|token| token.to_str().ok())
175 .unwrap_or_default();
176 let now = db.clock().now().timestamp();
177 keys.verify_principal_token(token, request.uri.path(), now)
178 .map_err(|refused| {
179 tracing::warn!(
180 http.route = request.uri.path(),
181 error.kind = "principal_token",
182 error = %refused,
183 "refused a call between apps"
184 );
185 Box::new(answer::refusal(
186 StatusCode::UNAUTHORIZED,
187 UNVERIFIED,
188 Caller::Json,
189 ))
190 })
191}
192
193async fn feed(db: &Db, query: Option<&str>) -> Result<Value> {
194 let number = |name: &str| {
195 query
196 .unwrap_or_default()
197 .split('&')
198 .filter_map(|pair| pair.split_once('='))
199 .find(|(key, _)| *key == name)
200 .map(|(_, value)| {
201 value
202 .parse::<i64>()
203 .map_err(|_| Error::BadRequest(format!("`{name}` is not a whole number")))
204 })
205 .transpose()
206 };
207 let after = number("after")?.unwrap_or(0);
208 let limit = number("limit")?.unwrap_or(sofie_db::FEED_PAGE);
209 answered(&sofie_db::read_feed(db, after, limit).await?)
210}
211
212fn json(result: Result<Value>) -> Response {
213 match result {
214 Ok(value) => answer::response(StatusCode::OK, Some(JSON), value.to_string()),
215 Err(error) => answer::failure(error, Caller::Json),
216 }
217}
218
219fn unprovided<T: ?Sized>() -> Error {
220 Error::internal_message(format!(
221 "nothing provides {}; run `sofie gen` so the wiring does",
222 std::any::type_name::<T>()
223 ))
224}