1use std::{fmt, time::Duration};
2
3use axum::http::{HeaderName, HeaderValue};
4use baukit_config::HttpConfig;
5
6use crate::error::is_valid_error_code;
7
8const DEFAULT_BODY_SIZE_LIMIT: usize = 2 * 1024 * 1024;
9const DEFAULT_CONCURRENCY_LIMIT: usize = 1_024;
10const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
11
12#[derive(Clone, Debug)]
18pub struct HttpOptions {
19 pub request_timeout: Duration,
21 pub body_size_limit: usize,
23 pub concurrency_limit: usize,
25 pub allow_credentials: bool,
27 pub(crate) allowed_origins: Vec<HeaderValue>,
28 pub(crate) additional_allowed_headers: Vec<HeaderName>,
29 pub(crate) json_rejection_code: String,
30}
31
32impl HttpOptions {
33 pub fn with_allowed_origins<I, S>(mut self, origins: I) -> Result<Self, HttpOptionsError>
37 where
38 I: IntoIterator<Item = S>,
39 S: AsRef<str>,
40 {
41 self.allowed_origins = parse_origins(origins)?;
42 Ok(self)
43 }
44
45 #[must_use]
47 pub fn allowed_origins(&self) -> &[HeaderValue] {
48 &self.allowed_origins
49 }
50
51 pub fn with_additional_allowed_headers<I, S>(
57 mut self,
58 headers: I,
59 ) -> Result<Self, HttpOptionsError>
60 where
61 I: IntoIterator<Item = S>,
62 S: AsRef<str>,
63 {
64 for header in headers {
65 let header = header.as_ref();
66 let parsed = HeaderName::from_bytes(header.as_bytes())
67 .map_err(|_| HttpOptionsError::InvalidHeaderName(header.to_owned()))?;
68 if !self.additional_allowed_headers.contains(&parsed) {
69 self.additional_allowed_headers.push(parsed);
70 }
71 }
72 Ok(self)
73 }
74
75 #[must_use]
77 pub fn additional_allowed_headers(&self) -> &[HeaderName] {
78 &self.additional_allowed_headers
79 }
80
81 pub fn with_json_rejection_code(
86 mut self,
87 code: impl Into<String>,
88 ) -> Result<Self, HttpOptionsError> {
89 let code = code.into();
90 if !is_valid_error_code(&code) {
91 return Err(HttpOptionsError::InvalidJsonRejectionCode(code));
92 }
93 self.json_rejection_code = code;
94 Ok(self)
95 }
96
97 #[must_use]
99 pub fn json_rejection_code(&self) -> &str {
100 &self.json_rejection_code
101 }
102
103 pub fn from_config(config: &HttpConfig) -> Result<Self, HttpOptionsError> {
108 Self {
109 request_timeout: config.request_timeout,
110 body_size_limit: config.body_size_limit,
111 concurrency_limit: config.concurrency_limit,
112 allow_credentials: false,
113 allowed_origins: parse_origins(&config.cors_allowed_origins)?,
114 additional_allowed_headers: Vec::new(),
115 json_rejection_code: "validation_failed".to_owned(),
116 }
117 .validate()
118 }
119
120 pub(crate) fn validate(self) -> Result<Self, HttpOptionsError> {
121 if self.request_timeout.is_zero() {
122 return Err(HttpOptionsError::ZeroRequestTimeout);
123 }
124 if self.body_size_limit == 0 {
125 return Err(HttpOptionsError::ZeroBodySizeLimit);
126 }
127 if self.concurrency_limit == 0 {
128 return Err(HttpOptionsError::ZeroConcurrencyLimit);
129 }
130 Ok(self)
131 }
132}
133
134impl Default for HttpOptions {
135 fn default() -> Self {
136 Self {
137 request_timeout: DEFAULT_REQUEST_TIMEOUT,
138 body_size_limit: DEFAULT_BODY_SIZE_LIMIT,
139 concurrency_limit: DEFAULT_CONCURRENCY_LIMIT,
140 allow_credentials: false,
141 allowed_origins: Vec::new(),
142 additional_allowed_headers: Vec::new(),
143 json_rejection_code: "validation_failed".to_owned(),
144 }
145 }
146}
147
148#[derive(Clone, Debug, Eq, PartialEq)]
150pub enum HttpOptionsError {
151 InvalidOrigin(String),
153 WildcardOrigin,
155 InvalidHeaderName(String),
157 InvalidJsonRejectionCode(String),
159 ZeroRequestTimeout,
161 ZeroBodySizeLimit,
163 ZeroConcurrencyLimit,
165}
166
167impl fmt::Display for HttpOptionsError {
168 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self {
170 Self::InvalidOrigin(origin) => write!(formatter, "invalid CORS origin `{origin}`"),
171 Self::WildcardOrigin => formatter.write_str(
172 "wildcard CORS origins are unsupported; provide explicit allowed origins",
173 ),
174 Self::InvalidHeaderName(name) => {
175 write!(formatter, "invalid CORS request header name `{name}`")
176 }
177 Self::InvalidJsonRejectionCode(code) => write!(
178 formatter,
179 "invalid JSON rejection error code `{code}`; expected snake_case"
180 ),
181 Self::ZeroRequestTimeout => formatter.write_str("request timeout must be non-zero"),
182 Self::ZeroBodySizeLimit => formatter.write_str("body size limit must be non-zero"),
183 Self::ZeroConcurrencyLimit => formatter.write_str("concurrency limit must be non-zero"),
184 }
185 }
186}
187
188impl std::error::Error for HttpOptionsError {}
189
190fn parse_origins<I, S>(origins: I) -> Result<Vec<HeaderValue>, HttpOptionsError>
191where
192 I: IntoIterator<Item = S>,
193 S: AsRef<str>,
194{
195 origins
196 .into_iter()
197 .map(|origin| {
198 let origin = origin.as_ref();
199 if origin == "*" {
200 return Err(HttpOptionsError::WildcardOrigin);
201 }
202 HeaderValue::from_str(origin)
203 .map_err(|_| HttpOptionsError::InvalidOrigin(origin.to_owned()))
204 })
205 .collect()
206}