a (hacky, wip) multi-tenant oidc-terminating reverse proxy, written in anger on top of pingora
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}