1use std::net::SocketAddr;
6
7use bytes::Bytes;
8use http_request_derive::HttpRequest;
9use http_request_derive_client::Client as _;
10use http_request_derive_client_reqwest::{ReqwestClient, ReqwestClientError};
11use http_request_derive_logging::HttpLogger;
12use itertools::Itertools as _;
13use opentalk_client_requests_api_v1::{auth::LoginGetRequest, response::ApiError};
14use opentalk_types_api_v1::auth::{GetLoginResponseBody, OidcProvider};
15use serde::{Deserialize, Serialize};
16use snafu::{ResultExt as _, Snafu, ensure};
17use url::Url;
18
19use crate::{
20 AuthenticatedClient, Authorization,
21 oidc::{OidcEndpoints, OidcWellKnownRequest},
22};
23
24const COMPATIBLE_VERSIONS: &[&str] = &["v1"];
25
26#[derive(Debug, Snafu)]
28pub enum ClientError {
29 #[snafu(display("Reqwest returned an error"))]
37 Reqwest {
38 source: ReqwestClientError,
40 },
41
42 #[snafu(display("The API server returned an error"))]
45 Api {
46 source: ApiError,
48 },
49
50 #[snafu(display(
52 "No compatible API version found under the well-known API endpoint {url}. This client is compatible with API versions: {compatible_versions}."
53 ))]
54 NoCompatibleApiVersion {
55 url: Url,
57
58 compatible_versions: String,
60 },
61
62 #[snafu(display("Invalid OIDC url found: {url:?}"))]
64 InvalidOidcUrl {
65 url: String,
67
68 source: url::ParseError,
70 },
71
72 #[snafu(display(
76 "Discovered url {url} which cannot be a base and therefore is not a valid controller API url"
77 ))]
78 InvalidUrlDiscovered {
79 url: Url,
81 },
82
83 #[snafu(display("Failed to parse a root certificate as PEM"))]
85 CertificatePem {
86 source: reqwest::Error,
88 },
89
90 #[snafu(display("Failed to build the HTTP client"))]
92 BuildReqwestClient {
93 source: reqwest::Error,
95 },
96}
97
98impl From<ReqwestClientError> for ClientError {
99 fn from(source: ReqwestClientError) -> Self {
100 Self::Reqwest { source }
101 }
102}
103
104impl From<ApiError> for ClientError {
105 fn from(source: ApiError) -> Self {
106 Self::Api { source }
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct Client {
113 inner: ReqwestClient,
114 #[allow(unused)]
115 oidc_url: Url,
116 #[allow(unused)]
117 api_url: Url,
118}
119
120impl Client {
121 pub fn builder(url: Url) -> ClientBuilder {
123 ClientBuilder::new(url)
124 }
125
126 pub async fn discover(url: Url) -> Result<Self, ClientError> {
128 Self::builder(url).discover().await
129 }
130
131 pub async fn discover_controller(url: Url) -> Result<Self, ClientError> {
133 Self::builder(url).discover_controller().await
134 }
135
136 async fn discover_from(mut client: ReqwestClient) -> Result<Self, ClientError> {
137 match client
138 .execute(WellKnownFrontendRequest)
139 .await
140 .context(ReqwestSnafu)?
141 {
142 WellKnownFrontendResponse::Found(WellKnownFrontendBody {
143 opentalk_controller: ControllerBaseInfo { base_url },
144 }) => {
145 client.set_base_url(base_url);
146 }
147 WellKnownFrontendResponse::NotFound => {}
148 };
149 Self::discover_controller_from(client).await
150 }
151
152 async fn discover_controller_from(mut client: ReqwestClient) -> Result<Self, ClientError> {
153 let WellKnownApiBody {
154 opentalk_api: ApiInfo { v1 },
155 } = client
156 .execute(WellKnownApiRequest)
157 .await
158 .context(ReqwestSnafu)?;
159
160 let Some(VersionedApiInfo { base_url }) = v1 else {
161 return NoCompatibleApiVersionSnafu {
162 url: client.base_url().clone(),
163 compatible_versions: COMPATIBLE_VERSIONS.iter().join(", "),
164 }
165 .fail();
166 };
167
168 let api_url = match Url::parse(&base_url) {
169 Ok(url) => {
170 ensure!(!url.cannot_be_a_base(), InvalidUrlDiscoveredSnafu { url });
171 url
172 }
173 Err(_e) => {
174 let segments = base_url.trim_start_matches('/');
175 let url = client.base_url().clone();
176 let mut url = url;
177 _ = url.path_segments_mut().unwrap().push(segments);
178 url
179 }
180 };
181
182 client.set_base_url(api_url.clone());
183
184 let GetLoginResponseBody { oidc } = client
185 .execute(LoginGetRequest)
186 .await
187 .context(ReqwestSnafu)?;
188
189 let oidc_url = oidc
190 .url
191 .parse()
192 .context(InvalidOidcUrlSnafu { url: oidc.url })?;
193
194 Ok(Self {
195 oidc_url,
196 api_url,
197 inner: client,
198 })
199 }
200
201 pub async fn get_oidc_endpoints(&self) -> Result<OidcEndpoints, ClientError> {
203 let oidc_client = self.inner.clone().with_base_url(self.oidc_url.clone());
204 let oidc_endpoints = oidc_client
205 .execute(OidcWellKnownRequest)
206 .await
207 .context(ReqwestSnafu)?;
208 Ok(oidc_endpoints)
209 }
210
211 pub async fn get_oidc_provider(&self) -> Result<OidcProvider, ClientError> {
213 let GetLoginResponseBody { oidc } = self
214 .inner
215 .execute(LoginGetRequest)
216 .await
217 .context(ReqwestSnafu)?;
218 Ok(oidc)
219 }
220
221 pub async fn execute<R: HttpRequest + Send>(
223 &self,
224 request: R,
225 ) -> Result<R::Response, ReqwestClientError> {
226 self.inner.execute(request).await
227 }
228
229 pub async fn execute_authorized<R: HttpRequest + Send, A: Authorization + Sync>(
231 &self,
232 request: R,
233 authorization: A,
234 ) -> Result<R::Response, ReqwestClientError> {
235 let authenticated_client = AuthenticatedClient::new(self.inner.clone(), authorization);
236 authenticated_client.execute(request).await
237 }
238
239 pub fn reqwest_client(&self) -> &ReqwestClient {
241 &self.inner
242 }
243}
244
245#[derive(Debug)]
252pub struct ClientBuilder {
253 url: Url,
254 logger: Option<HttpLogger>,
255 certs: Vec<Vec<u8>>,
256 dns_overrides: Vec<(String, SocketAddr)>,
257}
258
259impl ClientBuilder {
260 pub fn new(url: Url) -> Self {
264 ClientBuilder {
265 url,
266 logger: None,
267 certs: Vec::new(),
268 dns_overrides: Vec::new(),
269 }
270 }
271
272 pub async fn discover(self) -> Result<Client, ClientError> {
275 Client::discover_from(self.into_reqwest_client()?).await
276 }
277
278 pub async fn discover_controller(self) -> Result<Client, ClientError> {
281 Client::discover_controller_from(self.into_reqwest_client()?).await
282 }
283
284 pub fn with_logger(mut self, logger: HttpLogger) -> Self {
286 self.logger = Some(logger);
287 self
288 }
289
290 pub fn add_pem_cert(mut self, pem: &[u8]) -> Self {
292 self.certs.push(pem.to_vec());
293 self
294 }
295
296 pub fn add_dns_override(mut self, domain: &str, addr: SocketAddr) -> Self {
298 self.dns_overrides.push((domain.to_owned(), addr));
299 self
300 }
301
302 fn into_reqwest_client(self) -> Result<ReqwestClient, ClientError> {
303 let ClientBuilder {
304 url,
305 logger,
306 certs,
307 dns_overrides,
308 } = self;
309
310 let mut builder = reqwest::Client::builder();
311
312 for pem in &certs {
313 let cert = reqwest::Certificate::from_pem(pem).context(CertificatePemSnafu)?;
314 builder = builder.tls_certs_merge([cert]);
315 }
316 for (domain, addr) in &dns_overrides {
317 builder = builder.resolve(domain, *addr);
318 }
319
320 let client = builder.build().context(BuildReqwestClientSnafu)?;
321 let mut inner = ReqwestClient::from_reqwest_client(client, url);
322
323 if let Some(logger) = logger {
324 inner = inner.with_logger(logger);
325 }
326 Ok(inner)
327 }
328}
329#[derive(Debug, Clone, PartialEq, Eq, HttpRequest)]
330#[http_request(method="GET", response = WellKnownFrontendResponse, path=".well-known/opentalk/client")]
331struct WellKnownFrontendRequest;
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334struct ControllerBaseInfo {
335 pub base_url: Url,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339struct WellKnownFrontendBody {
340 pub opentalk_controller: ControllerBaseInfo,
341}
342
343enum WellKnownFrontendResponse {
344 NotFound,
345 Found(WellKnownFrontendBody),
346}
347
348impl http_request_derive::FromHttpResponse for WellKnownFrontendResponse {
349 fn from_http_response(
350 http_response: http::Response<Bytes>,
351 ) -> Result<Self, http_request_derive::Error>
352 where
353 Self: Sized,
354 {
355 match <WellKnownFrontendBody as http_request_derive::FromHttpResponse>::from_http_response(
356 http_response,
357 ) {
358 Ok(body) => Ok(Self::Found(body)),
359 Err(e) if e.is_not_found() => Ok(Self::NotFound),
360 Err(e) => Err(e),
361 }
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, HttpRequest)]
366#[http_request(method="GET", response = WellKnownApiBody, path=".well-known/opentalk/api")]
367struct WellKnownApiRequest;
368
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370struct VersionedApiInfo {
371 pub base_url: String,
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375struct ApiInfo {
376 pub v1: Option<VersionedApiInfo>,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380struct WellKnownApiBody {
381 pub opentalk_api: ApiInfo,
382}