1use futures::TryStreamExt;
7use futures_core::stream::Stream;
8use http_body_util::{BodyExt, Empty, Full, StreamBody, combinators::BoxBody};
9use hyper::http::StatusCode;
10use hyper::{Method, body::Body, body::Bytes};
11use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
12use hyper_util::client::legacy::{Client, connect::HttpConnector};
13use hyper_util::rt::TokioExecutor;
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15use std::borrow::Cow;
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::io::AsyncRead;
19use tokio::time::timeout;
20
21const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
23
24use crate::prelude::*;
25use cloudillo_types::action_types::CreateAction;
26use cloudillo_types::auth_adapter::AuthAdapter;
27use cloudillo_types::validation::{id_tag_to_ascii, validate_id_tag};
28
29fn to_boxed<B>(body: B) -> BoxBody<Bytes, Error>
30where
31 B: Body<Data = Bytes> + Send + Sync + 'static,
32 B::Error: Send + 'static,
33{
34 body.map_err(|_err| Error::NetworkError("body stream error".into())).boxed()
35}
36
37#[derive(Deserialize)]
38struct TokenData {
39 token: Box<str>,
40}
41
42#[derive(Deserialize)]
43struct TokenRes {
44 data: TokenData,
45}
46
47#[derive(Debug)]
49pub enum ConditionalResult<T> {
50 Modified { data: T, etag: Option<Box<str>> },
52 NotModified,
54}
55
56#[derive(Debug, Clone)]
57pub struct Request {
58 pub auth_adapter: Arc<dyn AuthAdapter>,
59 client: Client<HttpsConnector<HttpConnector>, BoxBody<Bytes, Error>>,
60 proxy_tokens: Arc<crate::ProxyTokenCache>,
61}
62
63impl Request {
64 pub fn new(
65 auth_adapter: Arc<dyn AuthAdapter>,
66 proxy_tokens: Arc<crate::ProxyTokenCache>,
67 ) -> ClResult<Self> {
68 let client = HttpsConnectorBuilder::new()
69 .with_native_roots()
70 .map_err(|_| Error::ConfigError("no native root CA certificates found".into()))?
71 .https_only()
72 .enable_http1()
73 .build();
74
75 Ok(Request {
76 auth_adapter,
77 client: Client::builder(TokioExecutor::new()).build(client),
78 proxy_tokens,
79 })
80 }
81
82 async fn timed_request(
84 &self,
85 req: hyper::Request<BoxBody<Bytes, Error>>,
86 ) -> ClResult<hyper::Response<hyper::body::Incoming>> {
87 timeout(REQUEST_TIMEOUT, self.client.request(req))
88 .await
89 .map_err(|_| Error::Timeout)?
90 .map_err(Error::from)
91 }
92
93 fn host_for(id_tag: &str) -> ClResult<Cow<'_, str>> {
105 if !validate_id_tag(id_tag) {
106 return Err(Error::ValidationError(format!(
107 "invalid id_tag for federation request: {id_tag}"
108 )));
109 }
110 id_tag_to_ascii(id_tag)
111 }
112
113 async fn collect_body(body: hyper::body::Incoming) -> ClResult<Bytes> {
115 timeout(REQUEST_TIMEOUT, body.collect())
116 .await
117 .map_err(|_| Error::Timeout)?
118 .map_err(|_| Error::NetworkError("body collection error".into()))
119 .map(http_body_util::Collected::to_bytes)
120 }
121
122 pub async fn create_proxy_token(
128 &self,
129 tn_id: TnId,
130 id_tag: &str,
131 subject: Option<&str>,
132 ) -> ClResult<Box<str>> {
133 let host = Self::host_for(id_tag)?;
134 let auth_token = self
135 .auth_adapter
136 .create_action_token(
137 tn_id,
138 CreateAction {
139 typ: "PROXY".into(),
140 audience_tag: Some(id_tag.into()),
141 expires_at: Some(Timestamp::from_now(60)), ..Default::default()
143 },
144 )
145 .await?;
146 let req = hyper::Request::builder()
147 .method(Method::GET)
148 .uri(format!(
149 "https://cl-o.{}/api/auth/access-token?token={}{}",
150 host,
151 auth_token,
152 if let Some(subject) = subject {
153 format!("&subject={}", subject)
154 } else {
155 String::new()
156 }
157 ))
158 .body(to_boxed(Empty::new()))?;
159 let res = self.timed_request(req).await?;
160 if !res.status().is_success() {
161 return Err(Error::PermissionDenied);
162 }
163 let parsed: TokenRes = serde_json::from_slice(&Self::collect_body(res.into_body()).await?)?;
164
165 Ok(parsed.data.token)
166 }
167
168 async fn get_or_mint_proxy_token(&self, tn_id: TnId, id_tag: &str) -> ClResult<Box<str>> {
172 if let Some(token) = self.proxy_tokens.get(tn_id, id_tag) {
173 return Ok(token);
174 }
175 let token = self.create_proxy_token(tn_id, id_tag, None).await?;
176 self.proxy_tokens.insert(tn_id, id_tag, token.clone());
177 Ok(token)
178 }
179
180 pub async fn get_bin(
181 &self,
182 tn_id: TnId,
183 id_tag: &str,
184 path: &str,
185 auth: bool,
186 ) -> ClResult<Bytes> {
187 let host = Self::host_for(id_tag)?;
188 let mut attempt = 0u8;
189 loop {
190 let req = hyper::Request::builder()
191 .method(Method::GET)
192 .uri(format!("https://cl-o.{}/api{}", host, path));
193 let req = if auth {
194 req.header(
195 "Authorization",
196 format!("Bearer {}", self.get_or_mint_proxy_token(tn_id, id_tag).await?),
197 )
198 } else {
199 req
200 };
201 let req = req.body(to_boxed(Empty::new()))?;
202 let res = self.timed_request(req).await?;
203 debug!(status = %res.status(), "federated GET response");
204 match res.status() {
205 StatusCode::OK => return Self::collect_body(res.into_body()).await,
206 StatusCode::NOT_FOUND => return Err(Error::NotFound),
207 StatusCode::GONE => return Err(Error::Gone),
208 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN if auth && attempt == 0 => {
209 debug!(id_tag = %id_tag, path = %path,
210 "auth rejected, refreshing cached token and retrying");
211 self.proxy_tokens.invalidate(tn_id, id_tag);
212 attempt += 1;
213 }
214 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
215 return Err(Error::PermissionDenied);
216 }
217 code => {
218 return Err(Error::NetworkError(format!("unexpected HTTP status: {}", code)));
219 }
220 }
221 }
222 }
223
224 pub async fn get_stream(
230 &self,
231 tn_id: TnId,
232 id_tag: &str,
233 path: &str,
234 auth: bool,
235 ) -> ClResult<impl AsyncRead + Send + Unpin + use<>> {
236 let host = Self::host_for(id_tag)?;
237 let mut attempt = 0u8;
238 loop {
239 let req = hyper::Request::builder()
240 .method(Method::GET)
241 .uri(format!("https://cl-o.{}/api{}", host, path));
242 let req = if auth {
243 let token = self.get_or_mint_proxy_token(tn_id, id_tag).await?;
244 debug!("Got proxy token (len={})", token.len());
245 req.header("Authorization", format!("Bearer {}", token))
246 } else {
247 req
248 };
249 let req = req.body(to_boxed(Empty::new()))?;
250 let res = self.timed_request(req).await?;
251 match res.status() {
252 StatusCode::OK => {
253 let stream = res.into_body().into_data_stream().map_err(std::io::Error::other);
254 return Ok(tokio_util::io::StreamReader::new(stream));
255 }
256 StatusCode::NOT_FOUND => return Err(Error::NotFound),
257 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN if auth && attempt == 0 => {
258 debug!(id_tag = %id_tag, path = %path,
259 "auth rejected on stream, refreshing cached token and retrying");
260 self.proxy_tokens.invalidate(tn_id, id_tag);
261 attempt += 1;
262 }
263 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
264 return Err(Error::PermissionDenied);
265 }
266 code => {
267 return Err(Error::NetworkError(format!("unexpected HTTP status: {}", code)));
268 }
269 }
270 }
271 }
272
273 pub async fn get<Res>(&self, tn_id: TnId, id_tag: &str, path: &str) -> ClResult<Res>
274 where
275 Res: DeserializeOwned,
276 {
277 let res = self.get_bin(tn_id, id_tag, path, true).await?;
278 let parsed: Res = serde_json::from_slice(&res)?;
279 Ok(parsed)
280 }
281
282 pub async fn get_noauth<Res>(&self, tn_id: TnId, id_tag: &str, path: &str) -> ClResult<Res>
283 where
284 Res: DeserializeOwned,
285 {
286 let res = self.get_bin(tn_id, id_tag, path, false).await?;
287 let parsed: Res = serde_json::from_slice(&res)?;
288 Ok(parsed)
289 }
290
291 pub async fn get_public<Res>(&self, id_tag: &str, path: &str) -> ClResult<Res>
293 where
294 Res: DeserializeOwned,
295 {
296 let host = Self::host_for(id_tag)?;
297 let req = hyper::Request::builder()
298 .method(Method::GET)
299 .uri(format!("https://cl-o.{}/api{}", host, path))
300 .body(to_boxed(Empty::new()))?;
301 let res = self.timed_request(req).await?;
302 match res.status() {
303 StatusCode::OK => {
304 let bytes = Self::collect_body(res.into_body()).await?;
305 let parsed: Res = serde_json::from_slice(&bytes)?;
306 Ok(parsed)
307 }
308 StatusCode::NOT_FOUND => Err(Error::NotFound),
309 StatusCode::FORBIDDEN => Err(Error::PermissionDenied),
310 code => Err(Error::NetworkError(format!("unexpected HTTP status: {}", code))),
311 }
312 }
313
314 pub async fn get_conditional<Res>(
319 &self,
320 id_tag: &str,
321 path: &str,
322 etag: Option<&str>,
323 ) -> ClResult<ConditionalResult<Res>>
324 where
325 Res: DeserializeOwned,
326 {
327 let host = Self::host_for(id_tag)?;
328 let mut builder = hyper::Request::builder()
329 .method(Method::GET)
330 .uri(format!("https://cl-o.{}/api{}", host, path));
331
332 if let Some(etag) = etag {
334 builder = builder.header("If-None-Match", etag);
335 }
336
337 let req = builder.body(to_boxed(Empty::new()))?;
338 let res = self.timed_request(req).await?;
339
340 match res.status() {
341 StatusCode::NOT_MODIFIED => Ok(ConditionalResult::NotModified),
342 StatusCode::OK => {
343 let new_etag = res
345 .headers()
346 .get("etag")
347 .and_then(|v| v.to_str().ok())
348 .map(|s| s.trim_matches('"').into());
349
350 let bytes = Self::collect_body(res.into_body()).await?;
351 let parsed: Res = serde_json::from_slice(&bytes)?;
352 Ok(ConditionalResult::Modified { data: parsed, etag: new_etag })
353 }
354 StatusCode::NOT_FOUND => Err(Error::NotFound),
355 StatusCode::FORBIDDEN => Err(Error::PermissionDenied),
356 code => Err(Error::NetworkError(format!("unexpected HTTP status: {}", code))),
357 }
358 }
359
360 pub async fn post_public<Req, Res>(&self, id_tag: &str, path: &str, data: &Req) -> ClResult<Res>
362 where
363 Req: Serialize,
364 Res: DeserializeOwned,
365 {
366 let host = Self::host_for(id_tag)?;
367 let json_data = serde_json::to_vec(data)?;
368 let req = hyper::Request::builder()
369 .method(Method::POST)
370 .uri(format!("https://cl-o.{}/api{}", host, path))
371 .header("Content-Type", "application/json")
372 .body(to_boxed(Full::from(json_data)))?;
373 let res = self.timed_request(req).await?;
374 match res.status() {
375 StatusCode::OK | StatusCode::CREATED => {
376 let bytes = Self::collect_body(res.into_body()).await?;
377 let parsed: Res = serde_json::from_slice(&bytes)?;
378 Ok(parsed)
379 }
380 StatusCode::NOT_FOUND => Err(Error::NotFound),
381 StatusCode::FORBIDDEN => Err(Error::PermissionDenied),
382 StatusCode::UNPROCESSABLE_ENTITY => Err(Error::ValidationError(
383 "IDP registration failed - validation error".to_string(),
384 )),
385 code => Err(Error::NetworkError(format!("unexpected HTTP status: {}", code))),
386 }
387 }
388
389 pub async fn post_bin(
395 &self,
396 _tn_id: TnId,
397 id_tag: &str,
398 path: &str,
399 data: Bytes,
400 ) -> ClResult<Bytes> {
401 let host = Self::host_for(id_tag)?;
402 let req = hyper::Request::builder()
403 .method(Method::POST)
404 .uri(format!("https://cl-o.{}/api{}", host, path))
405 .header("Content-Type", "application/json")
406 .body(to_boxed(Full::from(data)))?;
407 let res = self.timed_request(req).await?;
408 Self::collect_body(res.into_body()).await
409 }
410
411 pub async fn post_stream<S>(
414 &self,
415 _tn_id: TnId,
416 id_tag: &str,
417 path: &str,
418 stream: S,
419 ) -> ClResult<Bytes>
420 where
421 S: Stream<Item = Result<hyper::body::Frame<Bytes>, hyper::Error>> + Send + Sync + 'static,
422 {
423 let host = Self::host_for(id_tag)?;
424 let req = hyper::Request::builder()
425 .method(Method::POST)
426 .uri(format!("https://cl-o.{}/api{}", host, path))
427 .body(to_boxed(StreamBody::new(stream)))?;
428 let res = self.timed_request(req).await?;
429 Self::collect_body(res.into_body()).await
430 }
431
432 pub async fn post<Res>(
433 &self,
434 tn_id: TnId,
435 id_tag: &str,
436 path: &str,
437 data: &impl Serialize,
438 ) -> ClResult<Res>
439 where
440 Res: DeserializeOwned,
441 {
442 let res = self.post_bin(tn_id, id_tag, path, serde_json::to_vec(data)?.into()).await?;
443 let parsed: Res = serde_json::from_slice(&res)?;
444 Ok(parsed)
445 }
446
447 pub async fn post_json_authed(
457 &self,
458 tn_id: TnId,
459 id_tag: &str,
460 path: &str,
461 data: Bytes,
462 ) -> ClResult<Bytes> {
463 let host = Self::host_for(id_tag)?;
464 let mut attempt = 0u8;
465 loop {
466 let token = self.get_or_mint_proxy_token(tn_id, id_tag).await?;
467 let req = hyper::Request::builder()
468 .method(Method::POST)
469 .uri(format!("https://cl-o.{}/api{}", host, path))
470 .header("Content-Type", "application/json")
471 .header("Authorization", format!("Bearer {}", token))
472 .body(to_boxed(Full::from(data.clone())))?;
473 let res = self.timed_request(req).await?;
474 debug!(status = %res.status(), "federated POST response");
475 match res.status() {
476 StatusCode::NOT_FOUND => return Err(Error::NotFound),
477 StatusCode::GONE => return Err(Error::Gone),
478 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN if attempt == 0 => {
479 debug!(id_tag = %id_tag, path = %path,
480 "auth rejected on POST, refreshing cached token and retrying");
481 self.proxy_tokens.invalidate(tn_id, id_tag);
482 attempt += 1;
483 }
484 StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
485 return Err(Error::PermissionDenied);
486 }
487 code if code.is_success() => {
488 return Self::collect_body(res.into_body()).await;
493 }
494 code => {
495 return Err(Error::NetworkError(format!("unexpected HTTP status: {}", code)));
496 }
497 }
498 }
499 }
500
501 pub async fn post_authed<Res>(
503 &self,
504 tn_id: TnId,
505 id_tag: &str,
506 path: &str,
507 data: &impl Serialize,
508 ) -> ClResult<Res>
509 where
510 Res: DeserializeOwned,
511 {
512 let res = self
513 .post_json_authed(tn_id, id_tag, path, serde_json::to_vec(data)?.into())
514 .await?;
515 let parsed: Res = serde_json::from_slice(&res)?;
516 Ok(parsed)
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn host_for_accepts_valid() {
526 assert_eq!(Request::host_for("alice").unwrap(), "alice");
527 assert_eq!(Request::host_for("home.w9.hu").unwrap(), "home.w9.hu");
528 assert_eq!(Request::host_for("user-name-123").unwrap(), "user-name-123");
529 }
530
531 #[test]
532 fn host_for_punycodes_idn() {
533 assert_eq!(Request::host_for("münchen.example.com").unwrap(), "xn--mnchen-3ya.example.com");
535 }
536
537 #[test]
538 fn host_for_rejects_url_injection() {
539 for bad in [
542 "alice/../../etc",
543 "alice/admin",
544 "alice@evil.com",
545 "alice:8080",
546 "alice evil",
547 "alice?x=1",
548 "alice#frag",
549 "alice_123",
550 "Alice.Example.com",
551 "",
552 ] {
553 let err = Request::host_for(bad);
554 assert!(matches!(err, Err(Error::ValidationError(_))), "expected reject for {bad:?}");
555 }
556 }
557}
558
559