a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
1//! the actual gateway implementation
2
3use std::collections::HashMap;
4use std::ops::ControlFlow;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use cookie_rs::CookieJar;
9use http::status::StatusCode;
10use http::{HeaderName, HeaderValue};
11use pingora::prelude::*;
12use pingora::protocols::tls::CaType;
13use url::Url;
14
15use crate::config::format::claims::ClaimToHeader;
16use crate::gateway::oidc::{InProgressAuth, SESSION_COOKIE_NAME, UserInfo};
17use crate::httputil::{internal_error, internal_error_from, status_error, status_error_from};
18use crate::oauth::auth_code_flow;
19use crate::{config, cookies, httputil};
20
21pub mod oidc;
22
23/// per-domain information about backends and such
24pub struct DomainInfo {
25 /// the load balancer to use to select backends
26 pub balancer: Arc<LoadBalancer<super::SelectionChoice>>,
27 /// whether or not we allow insecure connections from clients
28 pub tls_mode: config::format::domain::TlsMode,
29 /// the sni name of this domain, used to pass to backends
30 pub sni_name: String,
31 /// auth settings for this domain, if any
32 pub oidc: Option<oidc::Info>,
33 /// headers to mangle for requests on this domain
34 pub headers: config::format::ManageHeaders,
35}
36
37/// the actual gateway logic
38pub struct AuthGateway {
39 /// all known domains and their corresponding backends & settings
40 pub domains: HashMap<String, DomainInfo>,
41}
42
43impl AuthGateway {
44 /// fetch the domain info for this request
45 fn domain_info<'s>(&'s self, session: &Session) -> Result<&'s DomainInfo> {
46 let req = session.req_header();
47 // TODO(potential-bug): afaict, right now, afaict, pingora a) does not check that SNI matches the `Host`
48 // header, b) does not support extracting the SNI info on rustls, so we'll have to switch
49 // to boringssl and implement that ourselves T_T
50 let host = req
51 .headers
52 .get(http::header::HOST)
53 .ok_or_else(status_error(
54 "no host set",
55 ErrorSource::Downstream,
56 StatusCode::BAD_REQUEST,
57 ))?
58 .to_str()
59 .map_err(|e| {
60 Error::because(
61 ErrorType::HTTPStatus(StatusCode::BAD_REQUEST.into()),
62 "no host",
63 e,
64 )
65 })?;
66 let info = self.domains.get(host).ok_or_else(status_error(
67 "unknown host",
68 ErrorSource::Downstream,
69 StatusCode::SERVICE_UNAVAILABLE,
70 ))?;
71
72 Ok(info)
73 }
74
75 /// mangle general headers, per [`config::format::ManageHeaders`]
76 async fn strip_and_apply_general_headers(
77 &self,
78 session: &mut Session,
79 info: &DomainInfo,
80 is_https: bool,
81 ) -> Result<()> {
82 let remote_addr = session.client_addr().and_then(|addr| match addr {
83 pingora::protocols::l4::socket::SocketAddr::Inet(socket_addr) => {
84 Some(socket_addr.ip().to_string())
85 }
86 pingora::protocols::l4::socket::SocketAddr::Unix(_) => None,
87 });
88 let req = session.req_header_mut();
89 if let Some(header) = &info.headers.host {
90 // TODO(cleanup): preprocess all header names
91 let name = HeaderName::from_bytes(header.as_bytes())
92 .map_err(internal_error_from("invalid claim-to-header header name"))?;
93 let val = req
94 .headers
95 .get(http::header::HOST)
96 .expect("we had to have this to look up our backend")
97 .clone();
98 req.insert_header(name, val)?;
99 }
100 if let Some(header) = &info.headers.x_forwarded_for
101 && let Some(addr) = &remote_addr
102 {
103 let name = HeaderName::from_bytes(header.as_bytes())
104 .map_err(internal_error_from("invalid claim-to-header header name"))?;
105 let val = req
106 .headers
107 .get("x-forwarded-for")
108 .map(|v| v.as_bytes())
109 .filter(|v| !v.is_empty())
110 .map(|v| v.to_owned())
111 .map(|mut v| {
112 v.extend(b",");
113 v.extend(addr.as_bytes());
114 v
115 })
116 .unwrap_or(addr.as_bytes().to_owned());
117 let val = HeaderValue::from_bytes(&val)
118 .map_err(internal_error_from("invalid remote-addr header value"))?;
119 req.insert_header(name, val)?;
120 }
121 if let Some(header) = &info.headers.x_forwarded_proto {
122 let name = HeaderName::from_bytes(header.as_bytes())
123 .map_err(internal_error_from("invalid claim-to-header header name"))?;
124 req.insert_header(
125 name,
126 HeaderValue::from_static(if is_https { "https" } else { "http" }),
127 )?;
128 }
129 if let Some(header) = &info.headers.remote_addr
130 && let Some(addr) = &remote_addr
131 {
132 let name = HeaderName::from_bytes(header.as_bytes())
133 .map_err(internal_error_from("invalid claim-to-header header name"))?;
134 let val = HeaderValue::from_str(addr)
135 .map_err(internal_error_from("invalid remote-addr header value"))?;
136 req.insert_header(name, val)?;
137 }
138
139 for header in &info.headers.always_clear {
140 req.remove_header(header);
141 }
142
143 Ok(())
144 }
145
146 /// check auth, starting the flow if necessary
147 async fn check_auth(
148 &self,
149 session: &mut Session,
150 auth_info: &oidc::Info,
151 ctx: &mut AuthCtx,
152 ) -> Result<ControlFlow<()>> {
153 use auth_code_flow::code_request;
154
155 let req = session.req_header_mut();
156 let cookies = httputil::cookie_jar(req)?.unwrap_or_default();
157
158 let auth_cookie = cookies
159 .get(SESSION_COOKIE_NAME)
160 .map(|c| c.value())
161 .and_then(|c| cookies::CookieContents::contents(c, &auth_info.cookie_signing_key).ok());
162 {
163 // auth_info map pin
164 let sessions = auth_info.sessions.pin();
165 if let Some(valid_session) = auth_cookie
166 .and_then(|c| sessions.get(&c.session_id))
167 .filter(|sess| sess.expires_at > jiff::Timestamp::now())
168 {
169 if let Some(claim_map) = &auth_info.config.claims {
170 for ClaimToHeader {
171 claim,
172 header,
173 serialize_as: _, // pre-serialized
174 } in &claim_map.claim_to_header
175 {
176 match valid_session.claims.get(claim) {
177 Some(val) => {
178 let val = HeaderValue::from_bytes(val.as_bytes())
179 .map_err(internal_error_from("invalid claim value"))?;
180 let name = HeaderName::from_bytes(header.as_bytes()).map_err(
181 internal_error_from("invalid claim-to-header header name"),
182 )?;
183 req.insert_header(name, val)?;
184 }
185 None => {
186 req.remove_header(header);
187 }
188 };
189 }
190 }
191
192 ctx.session_valid = true;
193
194 return Ok(ControlFlow::Continue(()));
195 }
196 }
197
198 // otherwise! start the auth flow
199 let meta_cache = auth_info.get_or_cache_metadata().await?;
200
201 // TODO(cleanup): precompute scopes
202 let redirect_info = code_request::redirect_to_auth_server(
203 (&meta_cache.metadata).into(),
204 code_request::Data::new(
205 &auth_info.config.client_id,
206 &auth_info
207 .config
208 .scopes
209 .as_ref()
210 .map(|s| {
211 s.required
212 .iter()
213 .fold(auth_code_flow::Scopes::base_scopes(), |scopes, scope| {
214 scopes.add_scope(scope)
215 })
216 })
217 .unwrap_or(auth_code_flow::Scopes::base_scopes()),
218 // technically this is a spec violate, but it's useful for testing
219 &Url::parse(&format!(
220 "https://{domain}/{path}",
221 domain = auth_info.domain,
222 path = OAUTH_CONTINUE_PATH,
223 ))
224 .map_err(internal_error_from(
225 "unable to construct redirect url from domain",
226 ))?,
227 ),
228 )
229 .map_err(internal_error_from("unable to construct redirect"))?;
230
231 if auth_info
232 .auth_states
233 .pin()
234 .try_insert(
235 redirect_info.state,
236 InProgressAuth {
237 code_verifier: redirect_info.code_verifier,
238 original_path: req.uri.path().to_string(),
239 },
240 )
241 .is_err()
242 {
243 // this is _extremely_ unlikely to happen, but worth checking anyway
244 return Err(internal_error("state id collision")());
245 };
246
247 httputil::redirect_response(session, redirect_info.url.as_str(), |_, _| Ok(())).await?;
248 Ok(ControlFlow::Break(()))
249 }
250
251 /// continue auth from inbound redirects, or logout from a logout redirect
252 async fn receive_redirect(
253 &self,
254 session: &mut Session,
255 info: &DomainInfo,
256 ) -> Result<ControlFlow<()>> {
257 use auth_code_flow::{code_response, token_request, token_response};
258
259 let req = session.req_header();
260 let Some(pq) = req.uri.path_and_query() else {
261 return Ok(ControlFlow::Continue(()));
262 };
263 let Some(auth_info) = &info.oidc else {
264 return Ok(ControlFlow::Continue(()));
265 };
266
267 if pq.path() == OAUTH_LOGOUT_PATH {
268 let Some(mut cookies) = httputil::cookie_jar(req)? else {
269 // we're not logged in, just return fine
270 httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| Ok(()))
271 .await?;
272 return Ok(ControlFlow::Break(()));
273 };
274
275 {
276 let Some(raw) = cookies
277 .get(SESSION_COOKIE_NAME)
278 .map(|raw| raw.value().to_string())
279 else {
280 // we're not logged in, just return fine
281 httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| {
282 Ok(())
283 })
284 .await?;
285 return Ok(ControlFlow::Break(()));
286 };
287 cookies.remove(SESSION_COOKIE_NAME);
288
289 let Some(cookies::CookieMessage { session_id }) =
290 cookies::CookieContents::contents(&raw, &auth_info.cookie_signing_key).ok()
291 else {
292 // invalid cookie, just ignore
293 httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| {
294 Ok(())
295 })
296 .await?;
297 return Ok(ControlFlow::Break(()));
298 };
299 auth_info.sessions.pin().remove(&session_id);
300 };
301
302 httputil::redirect_response(session, &auth_info.config.logout_url, |_, _| Ok(()))
303 .await?;
304 return Ok(ControlFlow::Break(()));
305 }
306
307 if let Some(auth_info) = &info.oidc
308 && pq.path().starts_with("/")
309 && &pq.path()[1..] == OAUTH_CONTINUE_PATH
310 {
311 let Some(query) = pq.query() else {
312 session
313 .respond_error(StatusCode::BAD_REQUEST.into())
314 .await?;
315 return Ok(ControlFlow::Break(()));
316 };
317
318 let Some(meta_cache) = auth_info.meta_cache.lock().await.as_ref().cloned() else {
319 // if we don't already have discovery metadata, something's real weird, cause
320 // how did we start the flow
321 session
322 .respond_error(StatusCode::BAD_REQUEST.into())
323 .await?;
324 return Ok(ControlFlow::Break(()));
325 };
326
327 let status =
328 code_response::receive_redirect(query, meta_cache.metadata.issuer.as_str())
329 .map_err(status_error_from(
330 "unable to deserialize oauth2 response",
331 ErrorSource::Internal,
332 StatusCode::BAD_REQUEST,
333 ))?;
334 let resp = match status {
335 Ok(resp) => resp,
336 Err(err) => {
337 auth_info.auth_states.pin().remove(&err.state);
338 match err.error {
339 code_response::ErrorType::AccessDenied => {
340 session.respond_error(StatusCode::FORBIDDEN.into()).await?;
341 return Ok(ControlFlow::Break(()));
342 }
343 code_response::ErrorType::TemporarilyUnavailable => {
344 session
345 .respond_error(StatusCode::SERVICE_UNAVAILABLE.into())
346 .await?;
347 return Ok(ControlFlow::Break(()));
348 }
349 _ => {
350 session
351 .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into())
352 .await?;
353 return Ok(ControlFlow::Break(()));
354 }
355 }
356 }
357 };
358
359 let Some(in_progress) = auth_info.auth_states.pin().remove(&resp.state).cloned() else {
360 session
361 .respond_error(StatusCode::BAD_REQUEST.into())
362 .await?;
363 return Ok(ControlFlow::Break(()));
364 };
365
366 let mut body = String::new();
367 let redirect_uri = &format!(
368 "https://{domain}/{path}",
369 domain = auth_info.domain,
370 path = OAUTH_CONTINUE_PATH,
371 );
372 let token_req = token_request::request_access_token(
373 (&meta_cache.metadata).into(),
374 token_request::Data {
375 code: resp,
376 client_id: &auth_info.config.client_id,
377 client_secret: &auth_info.client_secret,
378 // this should not be a clone, but it's a weird quirk of our threadsafe
379 // hashmap choice
380 code_verifier: in_progress.code_verifier,
381 redirect_uri,
382 },
383 &mut body,
384 )
385 .map_err(internal_error_from("unable produce access token request"))?;
386
387 let resp: token_response::Valid = {
388 let client = reqwest::Client::new();
389 let resp = client
390 .post(token_req.url.as_str().to_string())
391 .header(
392 http::header::CONTENT_TYPE,
393 "application/x-www-form-urlencoded",
394 )
395 .body(body)
396 .send()
397 .await
398 .map_err(internal_error_from("unable to make token request"))?;
399 if resp.status() == StatusCode::BAD_REQUEST {
400 let _resp: token_response::Error = resp
401 .json()
402 .await
403 .map_err(internal_error_from("unable to deserialize response"))?;
404 session
405 .respond_error(StatusCode::BAD_REQUEST.into())
406 .await?;
407 return Ok(ControlFlow::Break(()));
408 // error per [the rfc][ref:draft-ietf-oauth-v2-1#3.2.4]
409 } else if resp.status() == StatusCode::NOT_FOUND {
410 // maybe it moved? try fetching the info again later
411 auth_info.clear_metadata_cache().await;
412 } else if !resp.status().is_success() {
413 session
414 .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into())
415 .await?;
416 return Ok(ControlFlow::Break(()));
417 }
418 resp.json()
419 .await
420 .map_err(internal_error_from("unable to deserialize token response"))?
421 };
422
423 use std::str::FromStr as _;
424 let id_token = compact_jwt::JwtUnverified::from_str(
425 &resp
426 .id_token
427 .ok_or_else(internal_error("no id token in response"))?,
428 )
429 .map_err(internal_error_from("unable to deserialize id token"))?;
430
431 // will be some if we had the option "verify" turned on, will be none otherwise
432 // (will never be none if the option is on but we couldn't fetch the token)
433 let id_token: compact_jwt::Jwt<()> = match &meta_cache.jws_verifier {
434 Some(verifier) => {
435 use compact_jwt::JwsVerifier as _;
436 verifier
437 .verify(&id_token)
438 .map_err(internal_error_from("unable to verify id token"))?
439 }
440 None => {
441 use compact_jwt::JwsVerifier as _;
442 let verifier =
443 compact_jwt::dangernoverify::JwsDangerReleaseWithoutVerify::default();
444 verifier
445 .verify(&id_token)
446 .map_err(internal_error_from("unable to deserialize id_token to jwt"))?
447 }
448 };
449
450 // per https://openid.net/specs/openid-connect-core-1_0-final.html#TokenResponseValidation, we _must_ to check
451 // - iss (must match the expected issuer)
452 // - aud (must match our client_id)
453 // - exp (must expire in the future)
454 if id_token
455 .iss
456 .as_ref()
457 .is_none_or(|iss| iss != meta_cache.metadata.issuer.as_str())
458 {
459 return Err(internal_error("issuer mismatch on id token")());
460 }
461 if id_token
462 .aud
463 .as_ref()
464 .is_none_or(|aud| *aud != auth_info.config.client_id)
465 {
466 return Err(internal_error("audience mismatch on id token")());
467 }
468 let expires_at = jiff::Timestamp::from_second(
469 id_token
470 .exp
471 .ok_or_else(internal_error("missing exp on token"))?,
472 )
473 .map_err(internal_error_from("unable to parse exp as timestamp"))?;
474 if expires_at < jiff::Timestamp::now() {
475 session
476 .respond_error(StatusCode::INTERNAL_SERVER_ERROR.into())
477 .await?;
478 return Ok(ControlFlow::Break(()));
479 }
480
481 // reserialize, and then deserialize, to get the hard-coded claims out
482 let id_token: HashMap<_, _> = serde_json::from_value(
483 serde_json::to_value(id_token).expect("just deserialized from json"),
484 )
485 .expect("just round-tripped it");
486 let user_info = UserInfo {
487 expires_at,
488 claims: 'claims: {
489 let Some(claims_map) =
490 auth_info.config.claims.as_ref().map(|c| &c.claim_to_header)
491 else {
492 break 'claims Default::default();
493 };
494 id_token
495 .into_iter()
496 // [`serde_json::Value`] implements display that's just "semi-infallible
497 // serialize"
498 // ideally this'd be a hashmap in rust, but it shouldn't be too large,
499 // so it should be fine
500 .filter_map(|(k, v)| {
501 let claim_cfg =
502 claims_map.iter().find(|claim_cfg| claim_cfg.claim == k)?;
503 Some((k, v, claim_cfg))
504 })
505 .filter_map(|(k, v, cfg)| {
506 Some((k, httputil::serialize_claim(v, cfg.serialize_as.as_ref())?))
507 })
508 .collect()
509 },
510 };
511 let expiry = user_info.expires_at;
512 let mut rng = rand::rngs::OsRng;
513 use rand::Rng as _;
514 let session_id = rng.r#gen();
515 auth_info.sessions.pin().insert(session_id, user_info);
516 let cookie = cookies::CookieContents::sign(
517 cookies::CookieMessage { session_id },
518 &auth_info.cookie_signing_key,
519 )
520 .map_err(|()| internal_error("unable to sign cookie")())?;
521
522 let url = format!(
523 "https://{domain}{original_path}",
524 domain = auth_info.domain,
525 original_path = in_progress.original_path
526 );
527 httputil::redirect_response(session, &url, |resp, session| {
528 let mut cookies = httputil::cookie_jar(session.req_header())?.unwrap_or_default();
529 cookies.set(
530 cookie_rs::Cookie::builder(SESSION_COOKIE_NAME, cookie)
531 .http_only(true)
532 .secure(true)
533 // utc technically potentially different than gmt, but this is just advisory (we enforce
534 // elsewhere), so it's ok
535 .max_age(
536 std::time::Duration::try_from(expiry - jiff::Timestamp::now())
537 .expect("formed from timestamps, can't have relative parts"),
538 )
539 .path("/")
540 .build(),
541 );
542 cookies
543 .as_header_values()
544 .into_iter()
545 .try_for_each(|cookie| {
546 let val = HeaderValue::from_bytes(cookie.as_bytes())
547 .map_err(internal_error_from("bad cookie header value"))?;
548
549 resp.append_header(http::header::SET_COOKIE, val)?;
550 Ok::<_, Box<Error>>(())
551 })?;
552 Ok(())
553 })
554 .await?;
555 return Ok(ControlFlow::Break(()));
556 }
557
558 Ok(ControlFlow::Continue(()))
559 }
560}
561
562pub struct AuthCtx {
563 session_valid: bool,
564}
565
566/// the oauth2 redirect path, without the leading slash
567const OAUTH_CONTINUE_PATH: &str = ".oauth2/continue";
568/// the logout/cookie-clear path, _with_ the leading slash
569const OAUTH_LOGOUT_PATH: &str = "/.oauth2/logout";
570
571#[async_trait]
572impl ProxyHttp for AuthGateway {
573 type CTX = AuthCtx;
574 fn new_ctx(&self) -> Self::CTX {
575 AuthCtx {
576 session_valid: false,
577 }
578 }
579
580 async fn request_filter(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<bool> {
581 let info = self.domain_info(session)?;
582
583 // check if we need to terminate the connection cause someone sent us an http request and we
584 // don't allow that
585 let is_https = session
586 .digest()
587 .and_then(|d| d.ssl_digest.as_ref())
588 .is_some();
589 if !is_https {
590 use config::format::domain::TlsMode;
591 match info.tls_mode {
592 TlsMode::Only => {
593 // we should just drop the connection, although people should really just be
594 // using HSTS
595 session.shutdown().await;
596 return Ok(true);
597 }
598 TlsMode::UnsafeAllowHttp => {}
599 }
600 }
601
602 // next, check if we're in the middle of an oauth flow
603 match self.receive_redirect(session, info).await? {
604 ControlFlow::Continue(()) => {}
605 ControlFlow::Break(()) => return Ok(true),
606 }
607
608 // finally check our actual auth state, starting the auth flow as needed
609 if let Some(auth_info) = &info.oidc {
610 match self.check_auth(session, auth_info, ctx).await? {
611 ControlFlow::Continue(()) => {}
612 ControlFlow::Break(()) => return Ok(true),
613 }
614 }
615
616 // we're past auth and are processing as normal, proceed
617 self.strip_and_apply_general_headers(session, info, is_https)
618 .await?;
619
620 Ok(false)
621 }
622
623 async fn upstream_peer(
624 &self,
625 session: &mut Session,
626 _ctx: &mut Self::CTX,
627 ) -> Result<Box<HttpPeer>> {
628 fn client_addr_key(sock_addr: &pingora::protocols::l4::socket::SocketAddr) -> Vec<u8> {
629 use pingora::protocols::l4::socket::SocketAddr;
630 match sock_addr {
631 SocketAddr::Inet(socket_addr) => match socket_addr {
632 std::net::SocketAddr::V4(v4) => Vec::from(v4.ip().octets()),
633 std::net::SocketAddr::V6(v6) => Vec::from(v6.ip().octets()),
634 },
635 _ => unreachable!(),
636 }
637 }
638
639 let backends = self.domain_info(session)?;
640 let backend = backends
641 .balancer
642 // NB: this means that CGNAT, other proxies, etc will? consistently hit the same
643 // backend, so we might wanna take that into consideration. fine for now, this is
644 // currently for personal use ;-)
645 .select(
646 &client_addr_key(session.client_addr().ok_or_else(status_error(
647 "no client address",
648 ErrorSource::Downstream,
649 StatusCode::BAD_REQUEST,
650 ))?), /* lb on client address */
651 256,
652 )
653 .ok_or_else(status_error(
654 "no available backends",
655 ErrorSource::Upstream,
656 StatusCode::SERVICE_UNAVAILABLE,
657 ))?;
658
659 let backend_data = backend
660 .ext
661 .get::<BackendData>()
662 .cloned() // just some bools and an arc, it's fine
663 .unwrap_or_default();
664
665 let peer = match backend_data {
666 BackendData::HttpOnly => HttpPeer::new(backend, false, backends.sni_name.to_string()),
667 BackendData::Tls {
668 skip_verifying_cert,
669 ca,
670 } => {
671 let mut peer = HttpPeer::new(backend, true, backends.sni_name.to_string());
672 peer.options.verify_cert = !skip_verifying_cert;
673 peer.options.ca = ca;
674 peer
675 }
676 BackendData::HttpOverUds => {
677 HttpPeer::new_from_sockaddr(backend.addr, false, backends.sni_name.to_string())
678 }
679 };
680 Ok(Box::new(peer))
681 }
682
683 async fn response_filter(
684 &self,
685 _session: &mut Session,
686 upstream_response: &mut ResponseHeader,
687 ctx: &mut Self::CTX,
688 ) -> Result<()>
689 where
690 Self::CTX: Send + Sync,
691 {
692 // if we had no valid session, clear the cookie if set
693 if !ctx.session_valid {
694 let Some(mut cookies) = ('cookies: {
695 let Some(raw) = upstream_response.headers.get(http::header::COOKIE) else {
696 break 'cookies None;
697 };
698 Some(
699 raw.to_str()
700 .map_err(Box::<dyn ErrorTrait + Send + Sync>::from)
701 .and_then(|c| Ok(CookieJar::parse(c)?))
702 .map_err(status_error_from(
703 "bad cookie header",
704 ErrorSource::Downstream,
705 StatusCode::BAD_REQUEST,
706 ))?,
707 )
708 }) else {
709 return Ok(());
710 };
711 if cookies.get(SESSION_COOKIE_NAME).is_none() {
712 return Ok(());
713 }
714 cookies.remove(SESSION_COOKIE_NAME);
715
716 cookies.as_header_values().into_iter().try_for_each(|v| {
717 let v = HeaderValue::from_bytes(v.as_bytes())
718 .map_err(internal_error_from("invalid clear cookie header value"))?;
719
720 upstream_response
721 .headers
722 .append(http::header::SET_COOKIE, v);
723 Ok::<_, Box<Error>>(())
724 })?;
725 }
726 Ok(())
727 }
728}
729
730/// additional data stored in the load balancer's backend structure
731///
732/// for use in [`AuthGateway::upstream_peer`]
733#[derive(Clone)]
734pub enum BackendData {
735 HttpOverUds,
736 HttpOnly,
737 Tls {
738 /// should skip we verifying the cert (useful if the certs are selfsigned and we don't want to
739 /// have them loaded)
740 skip_verifying_cert: bool,
741 /// custom ca to use to verify backend certs
742 ca: Option<Arc<CaType>>,
743 },
744}
745impl Default for BackendData {
746 fn default() -> Self {
747 Self::Tls {
748 skip_verifying_cert: false,
749 ca: None,
750 }
751 }
752}