a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
at main 752 lines 30 kB view raw
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}