a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
at main 158 lines 4.7 kB view raw
1//! http utilities 2 3use http::StatusCode; 4use pingora::prelude::*; 5 6/// closure that returns the given status with no cause 7/// 8/// use with [`Option::ok_or_else`] 9pub fn status_error( 10 why: &'static str, 11 src: ErrorSource, 12 status: StatusCode, 13) -> impl FnOnce() -> Box<Error> { 14 move || { 15 Error::create( 16 ErrorType::HTTPStatus(status.into()), 17 src, 18 Some(why.into()), 19 None, 20 ) 21 } 22} 23/// closure that returns `500 Internal Server Error`, marked as caused by the error returned to 24/// the given closure 25/// 26/// use with [`Result::map_err`] 27pub fn internal_error_from<E>(why: &'static str) -> impl FnOnce(E) -> Box<Error> 28where 29 E: Into<Box<dyn ErrorTrait + Send + Sync>>, 30{ 31 move |cause| { 32 Error::create( 33 ErrorType::HTTPStatus(StatusCode::INTERNAL_SERVER_ERROR.into()), 34 ErrorSource::Internal, 35 Some(why.into()), 36 Some(cause.into()), 37 ) 38 } 39} 40 41/// closure that returns `500 Internal Server Error` with no cause 42/// 43/// use with [`Option::ok_or_else`] 44pub fn internal_error(why: &'static str) -> impl FnOnce() -> Box<Error> { 45 move || { 46 Error::create( 47 ErrorType::HTTPStatus(StatusCode::INTERNAL_SERVER_ERROR.into()), 48 ErrorSource::Internal, 49 Some(why.into()), 50 None, 51 ) 52 } 53} 54/// closure that returns the given status, marked as caused by the error returned to the given closure 55/// 56/// use with [`Result::map_err`] 57pub fn status_error_from<E>( 58 why: &'static str, 59 src: ErrorSource, 60 status: http::StatusCode, 61) -> impl FnOnce(E) -> Box<Error> 62where 63 E: Into<Box<dyn ErrorTrait + Send + Sync>>, 64{ 65 move |cause| { 66 Error::create( 67 ErrorType::HTTPStatus(status.into()), 68 src, 69 Some(why.into()), 70 Some(cause.into()), 71 ) 72 } 73} 74 75/// redirect to the given location 76/// 77/// the given callback can be used to inject additional headers, like `set-cookie` 78pub async fn redirect_response( 79 session: &mut Session, 80 to: &str, 81 bld_resp_header: impl FnOnce(&mut ResponseHeader, &Session) -> Result<()>, 82) -> Result<()> { 83 session.set_keepalive(None); 84 session 85 .write_response_header( 86 Box::new({ 87 // per <rfc:draft-ietf-oauth-v2-1#1.6>, any redirect is fine save 307, but HTTP 302 seems to 88 // be their example. 89 let mut resp = ResponseHeader::build(StatusCode::FOUND, Some(0))?; 90 resp.insert_header(http::header::LOCATION, to)?; 91 bld_resp_header(&mut resp, session)?; 92 resp 93 }), 94 true, 95 ) 96 .await?; 97 session.finish_body().await?; 98 Ok(()) 99} 100 101/// fetch the cookies for the current request 102pub fn cookie_jar(req: &'_ RequestHeader) -> Result<Option<cookie_rs::CookieJar<'_>>> { 103 use cookie_rs::CookieJar; 104 105 let Some(raw) = req.headers.get(http::header::COOKIE) else { 106 return Ok(None); 107 }; 108 Ok(Some( 109 raw.to_str() 110 .map_err(Box::<dyn ErrorTrait + Send + Sync>::from) 111 .and_then(|c| Ok(CookieJar::parse(c)?)) 112 .map_err(status_error_from( 113 "bad cookie header", 114 ErrorSource::Downstream, 115 StatusCode::BAD_REQUEST, 116 ))?, 117 )) 118} 119 120/// serialize a claim value for use in a header 121pub fn serialize_claim( 122 val: serde_json::Value, 123 cfg: Option<&crate::config::format::claims::Serialization>, 124) -> Option<String> { 125 let array_sep = cfg 126 .and_then(|c| c.join_array_items_with.as_deref()) 127 .unwrap_or(""); 128 let key_and_value_sep = cfg 129 .and_then(|c| c.join_keys_and_values_with.as_deref()) 130 .unwrap_or(""); 131 let map_item_sep = cfg 132 .and_then(|c| c.join_key_value_pairs_with.as_deref()) 133 .unwrap_or(""); 134 135 Some(match val { 136 serde_json::Value::Null => return None, 137 serde_json::Value::Bool(b) => b.to_string(), 138 serde_json::Value::Number(num) => num.to_string(), 139 serde_json::Value::String(s) => s, 140 serde_json::Value::Array(values) => values 141 .into_iter() 142 .filter_map(|val| serialize_claim(val, cfg)) 143 // T_T no intersperse yet 144 .collect::<Vec<_>>() 145 .join(array_sep), 146 serde_json::Value::Object(map) => map 147 .into_iter() 148 .filter_map(|(key, val)| { 149 Some(format!( 150 "{key}{sep}{val}", 151 sep = key_and_value_sep, 152 val = serialize_claim(val, cfg)?, 153 )) 154 }) 155 .collect::<Vec<_>>() 156 .join(map_item_sep), 157 }) 158}