1use core::future::Future;
4
5use super::{HeaderName, MediaType, ResponseWriter, StatusCode, TransportRequest};
6
7pub const MAX_INFORMATIONAL_RESPONSES: u8 = 8;
9pub const MAX_RESPONSE_CHUNKS: usize = 4_096;
11pub const MAX_RAW_RESPONSE_BODY_BYTES: usize = 64 * 1024 * 1024;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ResponseMediaPolicy<'a> {
17 Required(&'a [MediaType<'a>]),
19 Optional(&'a [MediaType<'a>]),
21 Forbidden,
23}
24
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub enum TrailerPolicy {
28 Reject,
30}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum RawResponsePolicyError {
35 InformationalLimitTooLarge,
37 BodyLimitTooLarge,
39 MissingMediaType,
41 DuplicateMediaType,
43 TooManyAdmittedHeaders,
45 DuplicateAdmittedHeader,
47 UnsafeAdmittedHeader,
49 ForbiddenMediaHasBodyLimit,
51}
52
53impl_static_error!(RawResponsePolicyError,
54 Self::InformationalLimitTooLarge => "informational response limit is too large",
55 Self::BodyLimitTooLarge => "raw response body limit is too large",
56 Self::MissingMediaType => "response media policy has no accepted media type",
57 Self::DuplicateMediaType => "response media policy contains duplicate media types",
58 Self::TooManyAdmittedHeaders => "too many response headers are admitted",
59 Self::DuplicateAdmittedHeader => "an admitted response header is duplicated",
60 Self::UnsafeAdmittedHeader => "an unsafe response header was admitted",
61 Self::ForbiddenMediaHasBodyLimit => "forbidden response media has a nonzero body limit",
62);
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub struct RawResponsePolicy<'a> {
67 success_body_bytes: usize,
68 error_body_bytes: usize,
69 success_media: ResponseMediaPolicy<'a>,
70 error_media: ResponseMediaPolicy<'a>,
71 admitted_headers: &'a [HeaderName<'a>],
72 informational_limit: u8,
73 trailer_policy: TrailerPolicy,
74}
75
76impl<'a> RawResponsePolicy<'a> {
77 pub fn new(
79 success_body_bytes: usize,
80 error_body_bytes: usize,
81 success_media: ResponseMediaPolicy<'a>,
82 error_media: ResponseMediaPolicy<'a>,
83 admitted_headers: &'a [HeaderName<'a>],
84 informational_limit: u8,
85 ) -> Result<Self, RawResponsePolicyError> {
86 if success_body_bytes > MAX_RAW_RESPONSE_BODY_BYTES
87 || error_body_bytes > MAX_RAW_RESPONSE_BODY_BYTES
88 {
89 return Err(RawResponsePolicyError::BodyLimitTooLarge);
90 }
91 if informational_limit > MAX_INFORMATIONAL_RESPONSES {
92 return Err(RawResponsePolicyError::InformationalLimitTooLarge);
93 }
94 validate_media(success_media)?;
95 validate_media(error_media)?;
96 if (matches!(success_media, ResponseMediaPolicy::Forbidden) && success_body_bytes != 0)
97 || (matches!(error_media, ResponseMediaPolicy::Forbidden) && error_body_bytes != 0)
98 {
99 return Err(RawResponsePolicyError::ForbiddenMediaHasBodyLimit);
100 }
101 validate_headers(admitted_headers)?;
102 Ok(Self {
103 success_body_bytes,
104 error_body_bytes,
105 success_media,
106 error_media,
107 admitted_headers,
108 informational_limit,
109 trailer_policy: TrailerPolicy::Reject,
110 })
111 }
112
113 #[must_use]
115 pub const fn body_limit(self, status: StatusCode) -> usize {
116 if status.is_success() {
117 self.success_body_bytes
118 } else {
119 self.error_body_bytes
120 }
121 }
122
123 #[must_use]
125 pub const fn media_policy(self, status: StatusCode) -> ResponseMediaPolicy<'a> {
126 if status.is_success() {
127 self.success_media
128 } else {
129 self.error_media
130 }
131 }
132
133 #[must_use]
135 pub fn admits_header(self, name: &str) -> bool {
136 self.admitted_headers
137 .iter()
138 .any(|candidate| candidate.eq_ignore_ascii_case(name))
139 }
140
141 #[must_use]
143 pub const fn informational_limit(self) -> u8 {
144 self.informational_limit
145 }
146
147 #[must_use]
149 pub const fn trailer_policy(self) -> TrailerPolicy {
150 self.trailer_policy
151 }
152}
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
156pub enum InformationalResponseError {
157 SwitchingProtocols,
159 TooManyInformationalResponses,
161 MissingFinalResponse,
163}
164
165impl_static_error!(InformationalResponseError,
166 Self::SwitchingProtocols => "switching protocols is forbidden",
167 Self::TooManyInformationalResponses => "too many informational responses",
168 Self::MissingFinalResponse => "final HTTP response is missing",
169);
170
171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub struct InformationalResponseTracker {
174 limit: u8,
175 observed: u8,
176}
177
178impl InformationalResponseTracker {
179 #[must_use]
181 pub const fn new(policy: RawResponsePolicy<'_>) -> Self {
182 Self {
183 limit: policy.informational_limit,
184 observed: 0,
185 }
186 }
187
188 pub fn observe(&mut self, status: StatusCode) -> Result<bool, InformationalResponseError> {
190 if status.get() == 101 {
191 return Err(InformationalResponseError::SwitchingProtocols);
192 }
193 if status.get() < 200 {
194 self.observed = self
195 .observed
196 .checked_add(1)
197 .ok_or(InformationalResponseError::TooManyInformationalResponses)?;
198 if self.observed > self.limit {
199 return Err(InformationalResponseError::TooManyInformationalResponses);
200 }
201 return Ok(false);
202 }
203 Ok(true)
204 }
205
206 #[must_use]
208 pub const fn observed(self) -> u8 {
209 self.observed
210 }
211}
212
213pub trait BlockingRawHttpExecutor {
215 type Error;
217
218 fn execute(
223 &self,
224 request: TransportRequest<'_>,
225 policy: RawResponsePolicy<'_>,
226 response: &mut ResponseWriter<'_>,
227 ) -> Result<(), Self::Error>;
228}
229
230pub trait AsyncRawHttpExecutor {
232 type Error;
234
235 fn execute<'executor, 'request, 'policy, 'writer>(
241 &'executor self,
242 request: TransportRequest<'request>,
243 policy: RawResponsePolicy<'policy>,
244 response: &'writer mut ResponseWriter<'_>,
245 ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'writer
246 where
247 'executor: 'writer,
248 'request: 'writer,
249 'policy: 'writer;
250}
251
252fn validate_media(policy: ResponseMediaPolicy<'_>) -> Result<(), RawResponsePolicyError> {
253 let media = match policy {
254 ResponseMediaPolicy::Required(media) | ResponseMediaPolicy::Optional(media) => media,
255 ResponseMediaPolicy::Forbidden => return Ok(()),
256 };
257 if media.is_empty() {
258 return Err(RawResponsePolicyError::MissingMediaType);
259 }
260 for (index, value) in media.iter().enumerate() {
261 if media.get(..index).is_some_and(|seen| {
262 seen.iter()
263 .any(|candidate| candidate.as_str().eq_ignore_ascii_case(value.as_str()))
264 }) {
265 return Err(RawResponsePolicyError::DuplicateMediaType);
266 }
267 }
268 Ok(())
269}
270
271fn validate_headers(headers: &[HeaderName<'_>]) -> Result<(), RawResponsePolicyError> {
272 if headers.len() > super::MAX_RESPONSE_HEADERS {
273 return Err(RawResponsePolicyError::TooManyAdmittedHeaders);
274 }
275 for (index, header) in headers.iter().enumerate() {
276 if is_unsafe_response_header(header.as_str()) {
277 return Err(RawResponsePolicyError::UnsafeAdmittedHeader);
278 }
279 if headers.get(..index).is_some_and(|seen| {
280 seen.iter()
281 .any(|candidate| candidate.eq_ignore_ascii_case(header.as_str()))
282 }) {
283 return Err(RawResponsePolicyError::DuplicateAdmittedHeader);
284 }
285 }
286 Ok(())
287}
288
289fn is_unsafe_response_header(name: &str) -> bool {
290 [
291 "authorization",
292 "connection",
293 "cookie",
294 "proxy-authenticate",
295 "proxy-authorization",
296 "set-cookie",
297 "te",
298 "trailer",
299 "transfer-encoding",
300 "upgrade",
301 ]
302 .iter()
303 .any(|candidate| name.eq_ignore_ascii_case(candidate))
304}
305
306#[cfg(test)]
307mod tests {
308 use super::{
309 InformationalResponseError, InformationalResponseTracker, RawResponsePolicy,
310 RawResponsePolicyError, ResponseMediaPolicy,
311 };
312 use crate::transport::{HeaderName, MediaType, StatusCode};
313
314 fn policy(limit: u8) -> Result<RawResponsePolicy<'static>, RawResponsePolicyError> {
315 RawResponsePolicy::new(
316 1024,
317 256,
318 ResponseMediaPolicy::Required(&[MediaType::JSON]),
319 ResponseMediaPolicy::Optional(&[MediaType::JSON]),
320 &[],
321 limit,
322 )
323 }
324
325 #[test]
326 fn selects_independent_success_and_error_limits() {
327 let Ok(policy) = policy(2) else {
328 return;
329 };
330 assert_eq!(policy.body_limit(StatusCode::OK), 1024);
331 assert_eq!(
332 policy.body_limit(StatusCode::new(400).unwrap_or(StatusCode::TOO_MANY_REQUESTS)),
333 256
334 );
335 }
336
337 #[test]
338 fn bounds_informationals_and_rejects_switching_protocols() {
339 let Ok(policy) = policy(2) else {
340 return;
341 };
342 let mut tracker = InformationalResponseTracker::new(policy);
343 let early = StatusCode::new(103).unwrap_or(StatusCode::OK);
344 assert_eq!(tracker.observe(early), Ok(false));
345 assert_eq!(tracker.observe(early), Ok(false));
346 assert_eq!(
347 tracker.observe(early),
348 Err(InformationalResponseError::TooManyInformationalResponses)
349 );
350 let switching = StatusCode::new(101).unwrap_or(StatusCode::OK);
351 assert_eq!(
352 InformationalResponseTracker::new(policy).observe(switching),
353 Err(InformationalResponseError::SwitchingProtocols)
354 );
355 }
356
357 #[test]
358 fn rejects_unsafe_and_duplicate_admitted_headers() {
359 let unsafe_header = HeaderName::new("set-cookie");
360 assert!(unsafe_header.is_ok());
361 if let Ok(unsafe_header) = unsafe_header {
362 assert!(matches!(
363 RawResponsePolicy::new(
364 1,
365 1,
366 ResponseMediaPolicy::Optional(&[MediaType::JSON]),
367 ResponseMediaPolicy::Optional(&[MediaType::JSON]),
368 &[unsafe_header],
369 0,
370 ),
371 Err(RawResponsePolicyError::UnsafeAdmittedHeader)
372 ));
373 }
374 let first = HeaderName::new("x-request-id");
375 let second = HeaderName::new("X-Request-ID");
376 assert!(first.is_ok() && second.is_ok());
377 if let (Ok(first), Ok(second)) = (first, second) {
378 assert!(matches!(
379 RawResponsePolicy::new(
380 1,
381 1,
382 ResponseMediaPolicy::Optional(&[MediaType::JSON]),
383 ResponseMediaPolicy::Optional(&[MediaType::JSON]),
384 &[first, second],
385 0,
386 ),
387 Err(RawResponsePolicyError::DuplicateAdmittedHeader)
388 ));
389 }
390 }
391}