1use cloud_sdk::Method;
2use cloud_sdk::transport::{
3 ContentType, HeaderSensitivity, RawResponsePolicy, ResponseHeaders, ResponseMediaPolicy,
4 StatusCode, TrailerPolicy, TransportFailure,
5};
6use core::ops::Range;
7use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderMap, TRAILER};
8
9pub const MAX_UPSTREAM_HTTP1_HEADERS: usize = 100;
11pub const MAX_UPSTREAM_HTTP1_HEAD_BYTES: usize = 64 * 1024;
13pub const MAX_RAW_REQUEST_BODY_BYTES: usize = cloud_sdk::operation::LARGE_BODY_BYTES;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum RawHttpError {
19 ResponseAlreadyCommitted,
21 TargetRejected,
23 MethodRejected,
25 MissingContentType,
27 HeaderRejected,
29 RequestHeaderAllocationFailed,
31 RequestBodyAllocationFailed,
33 RequestBodyTooLarge,
35 RequestBuildFailed,
37 RuntimeInitializationFailed,
39 BlockingRuntimeContext,
41 ConnectFailed,
43 TimedOut,
45 RequestFailed,
47 ResponseOriginChanged,
49 InvalidStatus,
51 SwitchingProtocols,
53 TooManyInformationalResponses,
55 ResponseHeadTooLarge,
57 DuplicateResponseHeader,
59 ResponseTrailersRejected,
61 InvalidNoBodyFraming,
63 MissingResponseContentType,
65 InvalidResponseContentType,
67 UnexpectedResponseContentType,
69 ForbiddenResponseContentType,
71 InvalidResponseHeader,
73 ResponseTooLarge,
75 ResponseChunkLimitExceeded,
77 ResponseReadFailed,
79 ResponseCommitFailed,
81}
82
83impl_static_error!(RawHttpError,
84 Self::ResponseAlreadyCommitted => "response writer is already committed",
85 Self::TargetRejected => "request target was rejected",
86 Self::MethodRejected => "request method was rejected",
87 Self::MissingContentType => "request body content type is missing",
88 Self::HeaderRejected => "request header was rejected",
89 Self::RequestHeaderAllocationFailed => "request-header allocation failed",
90 Self::RequestBodyAllocationFailed => "request-body allocation failed",
91 Self::RequestBodyTooLarge => "request body is too large",
92 Self::RequestBuildFailed => "raw request construction failed",
93 Self::RuntimeInitializationFailed => "blocking executor initialization failed",
94 Self::BlockingRuntimeContext => "blocking executor called from an async runtime",
95 Self::ConnectFailed => "connection failed",
96 Self::TimedOut => "request timed out",
97 Self::RequestFailed => "request failed",
98 Self::ResponseOriginChanged => "response origin changed",
99 Self::InvalidStatus => "response status is invalid",
100 Self::SwitchingProtocols => "switching protocols is forbidden",
101 Self::TooManyInformationalResponses => "too many informational responses",
102 Self::ResponseHeadTooLarge => "response head exceeds wire limits",
103 Self::DuplicateResponseHeader => "response header is duplicated",
104 Self::ResponseTrailersRejected => "response trailers are rejected",
105 Self::InvalidNoBodyFraming => "no-body response framing is invalid",
106 Self::MissingResponseContentType => "response content type is missing",
107 Self::InvalidResponseContentType => "response content type is invalid",
108 Self::UnexpectedResponseContentType => "response content type is not admitted",
109 Self::ForbiddenResponseContentType => "response content type is forbidden",
110 Self::InvalidResponseHeader => "retained response header is invalid",
111 Self::ResponseTooLarge => "response body exceeds its status-class limit",
112 Self::ResponseChunkLimitExceeded => "response chunk limit is exceeded",
113 Self::ResponseReadFailed => "response body read failed",
114 Self::ResponseCommitFailed => "response commitment failed",
115);
116
117pub type RawTransportFailure = TransportFailure<RawHttpError>;
119
120pub(crate) struct ResponseBodyBudget {
121 limit: usize,
122 len: usize,
123 chunks: usize,
124}
125
126impl ResponseBodyBudget {
127 pub(crate) const fn new(limit: usize) -> Self {
128 Self {
129 limit,
130 len: 0,
131 chunks: 0,
132 }
133 }
134
135 pub(crate) fn observe(&mut self, bytes: usize) -> Result<Range<usize>, RawHttpError> {
136 self.chunks = self
137 .chunks
138 .checked_add(1)
139 .ok_or(RawHttpError::ResponseChunkLimitExceeded)?;
140 if self.chunks > cloud_sdk::transport::MAX_RESPONSE_CHUNKS {
141 return Err(RawHttpError::ResponseChunkLimitExceeded);
142 }
143 let end = self
144 .len
145 .checked_add(bytes)
146 .ok_or(RawHttpError::ResponseTooLarge)?;
147 if end > self.limit {
148 return Err(RawHttpError::ResponseTooLarge);
149 }
150 let range = self.len..end;
151 self.len = end;
152 Ok(range)
153 }
154
155 pub(crate) const fn len(&self) -> usize {
156 self.len
157 }
158}
159
160pub(crate) fn inspect_response_head(
161 method: Method,
162 status: StatusCode,
163 source: &HeaderMap,
164 policy: RawResponsePolicy<'_>,
165 captured: &mut ResponseHeaders<'_>,
166 writer_capacity: usize,
167) -> Result<usize, RawHttpError> {
168 validate_wire_head(source)?;
169 if status.get() == 101 {
170 return Err(RawHttpError::SwitchingProtocols);
171 }
172 if status.get() < 200 {
173 return Err(RawHttpError::InvalidStatus);
174 }
175 if matches!(policy.trailer_policy(), TrailerPolicy::Reject) && source.contains_key(TRAILER) {
176 return Err(RawHttpError::ResponseTrailersRejected);
177 }
178
179 let policy_limit = policy.body_limit(status);
180 let body_forbidden = method == Method::Head || matches!(status.get(), 204 | 304);
181 if status.get() == 204 && source.contains_key(CONTENT_LENGTH) {
182 return Err(RawHttpError::InvalidNoBodyFraming);
183 }
184 validate_media(source, policy.media_policy(status))?;
185 let selected_limit = if body_forbidden {
186 0
187 } else {
188 core::cmp::min(policy_limit, writer_capacity)
189 };
190 if let Some(declared) = declared_content_length(source)? {
191 let declared = usize::try_from(declared).map_err(|_| RawHttpError::ResponseTooLarge)?;
192 if !body_forbidden && declared > selected_limit {
193 return Err(RawHttpError::ResponseTooLarge);
194 }
195 }
196
197 for name in source.keys() {
198 if !policy.admits_header(name.as_str()) {
199 continue;
200 }
201 let Some(value) = source.get(name) else {
202 return Err(RawHttpError::InvalidResponseHeader);
203 };
204 let sensitivity = if is_reviewed_public(name.as_str()) {
205 HeaderSensitivity::Public
206 } else {
207 HeaderSensitivity::Sensitive
208 };
209 captured
210 .try_push(name.as_str(), value.as_bytes(), sensitivity)
211 .map_err(|_| RawHttpError::InvalidResponseHeader)?;
212 }
213 Ok(selected_limit)
214}
215
216fn validate_wire_head(headers: &HeaderMap) -> Result<(), RawHttpError> {
217 if headers.len() > MAX_UPSTREAM_HTTP1_HEADERS {
218 return Err(RawHttpError::ResponseHeadTooLarge);
219 }
220 let mut encoded_len = 0_usize;
221 for name in headers.keys() {
222 let values = headers.get_all(name);
223 if values.iter().count() != 1 {
224 return Err(RawHttpError::DuplicateResponseHeader);
225 }
226 let Some(value) = values.iter().next() else {
227 return Err(RawHttpError::InvalidResponseHeader);
228 };
229 encoded_len = encoded_len
230 .checked_add(name.as_str().len())
231 .and_then(|length| length.checked_add(value.as_bytes().len()))
232 .and_then(|length| length.checked_add(4))
233 .ok_or(RawHttpError::ResponseHeadTooLarge)?;
234 if encoded_len > MAX_UPSTREAM_HTTP1_HEAD_BYTES {
235 return Err(RawHttpError::ResponseHeadTooLarge);
236 }
237 }
238 Ok(())
239}
240
241fn declared_content_length(headers: &HeaderMap) -> Result<Option<u64>, RawHttpError> {
242 let Some(value) = headers.get(CONTENT_LENGTH) else {
243 return Ok(None);
244 };
245 let text = value
246 .to_str()
247 .map_err(|_| RawHttpError::InvalidNoBodyFraming)?;
248 text.parse::<u64>()
249 .map(Some)
250 .map_err(|_| RawHttpError::InvalidNoBodyFraming)
251}
252
253fn validate_media(
254 headers: &HeaderMap,
255 policy: ResponseMediaPolicy<'_>,
256) -> Result<(), RawHttpError> {
257 let content_type = headers.get(CONTENT_TYPE);
258 match (policy, content_type) {
259 (ResponseMediaPolicy::Required(_), None) => Err(RawHttpError::MissingResponseContentType),
260 (ResponseMediaPolicy::Optional(_), None) | (ResponseMediaPolicy::Forbidden, None) => Ok(()),
261 (ResponseMediaPolicy::Forbidden, Some(_)) => {
262 Err(RawHttpError::ForbiddenResponseContentType)
263 }
264 (
265 ResponseMediaPolicy::Required(admitted) | ResponseMediaPolicy::Optional(admitted),
266 Some(value),
267 ) => {
268 let text = value
269 .to_str()
270 .map_err(|_| RawHttpError::InvalidResponseContentType)?;
271 let parsed =
272 ContentType::new(text).map_err(|_| RawHttpError::InvalidResponseContentType)?;
273 if admitted.iter().any(|media| parsed.matches(*media)) {
274 Ok(())
275 } else {
276 Err(RawHttpError::UnexpectedResponseContentType)
277 }
278 }
279 }
280}
281
282fn is_reviewed_public(name: &str) -> bool {
283 ["content-length", "content-type", "date"]
284 .iter()
285 .any(|candidate| name.eq_ignore_ascii_case(candidate))
286}
287
288#[cfg(test)]
289mod tests {
290 use std::format;
291
292 use cloud_sdk::transport::{
293 HeaderName, MediaType, RawResponsePolicy, ResponseHeaders, ResponseMediaPolicy, StatusCode,
294 };
295 use reqwest::header::{HeaderMap, HeaderValue};
296
297 use super::{RawHttpError, inspect_response_head};
298
299 fn policy<'a>(headers: &'a [HeaderName<'a>]) -> Option<RawResponsePolicy<'a>> {
300 RawResponsePolicy::new(
301 8,
302 4,
303 ResponseMediaPolicy::Required(&[MediaType::JSON]),
304 ResponseMediaPolicy::Optional(&[MediaType::JSON]),
305 headers,
306 2,
307 )
308 .ok()
309 }
310
311 #[test]
312 fn selects_status_limit_and_drops_unadmitted_headers() {
313 let admitted = HeaderName::new("content-type");
314 assert!(admitted.is_ok());
315 let Ok(admitted) = admitted else { return };
316 let admitted_headers = [admitted];
317 let Some(policy) = policy(&admitted_headers) else {
318 return;
319 };
320 let mut source = HeaderMap::new();
321 source.insert("content-type", HeaderValue::from_static("application/json"));
322 source.insert("set-cookie", HeaderValue::from_static("secret=1"));
323 source.insert("x-unknown", HeaderValue::from_static("secret"));
324 let mut storage = [0_u8; 128];
325 let mut captured = ResponseHeaders::new(&mut storage);
326 let result = inspect_response_head(
327 cloud_sdk::Method::Get,
328 StatusCode::OK,
329 &source,
330 policy,
331 &mut captured,
332 16,
333 );
334 assert_eq!(result, Ok(8));
335 assert!(captured.get("content-type").is_some());
336 assert!(captured.get("set-cookie").is_none());
337 assert!(captured.get("x-unknown").is_none());
338 }
339
340 #[test]
341 fn rejects_duplicates_and_no_content_framing() {
342 let Some(policy) = policy(&[]) else { return };
343 let mut duplicate = HeaderMap::new();
344 duplicate.append("x-test", HeaderValue::from_static("one"));
345 duplicate.append("x-test", HeaderValue::from_static("two"));
346 let mut storage = [0_u8; 128];
347 let mut captured = ResponseHeaders::new(&mut storage);
348 assert_eq!(
349 inspect_response_head(
350 cloud_sdk::Method::Get,
351 StatusCode::new(400).unwrap_or(StatusCode::TOO_MANY_REQUESTS),
352 &duplicate,
353 policy,
354 &mut captured,
355 16,
356 ),
357 Err(RawHttpError::DuplicateResponseHeader)
358 );
359
360 let mut no_content = HeaderMap::new();
361 no_content.insert("content-length", HeaderValue::from_static("0"));
362 assert_eq!(
363 inspect_response_head(
364 cloud_sdk::Method::Get,
365 StatusCode::NO_CONTENT,
366 &no_content,
367 policy,
368 &mut captured,
369 16,
370 ),
371 Err(RawHttpError::InvalidNoBodyFraming)
372 );
373 }
374
375 #[test]
376 fn rejects_media_mismatch_oversized_length_and_hostile_header_count() {
377 let Some(policy) = policy(&[]) else { return };
378 let mut storage = [0_u8; 128];
379 let mut captured = ResponseHeaders::new(&mut storage);
380
381 let mut wrong_media = HeaderMap::new();
382 wrong_media.insert("content-type", HeaderValue::from_static("text/plain"));
383 assert_eq!(
384 inspect_response_head(
385 cloud_sdk::Method::Get,
386 StatusCode::OK,
387 &wrong_media,
388 policy,
389 &mut captured,
390 16,
391 ),
392 Err(RawHttpError::UnexpectedResponseContentType)
393 );
394
395 let mut oversized = HeaderMap::new();
396 oversized.insert("content-type", HeaderValue::from_static("application/json"));
397 oversized.insert("content-length", HeaderValue::from_static("9"));
398 assert_eq!(
399 inspect_response_head(
400 cloud_sdk::Method::Get,
401 StatusCode::OK,
402 &oversized,
403 policy,
404 &mut captured,
405 16,
406 ),
407 Err(RawHttpError::ResponseTooLarge)
408 );
409
410 let mut hostile = HeaderMap::new();
411 for index in 0..=super::MAX_UPSTREAM_HTTP1_HEADERS {
412 let name = format!("x-field-{index}");
413 let Ok(name) = reqwest::header::HeaderName::from_bytes(name.as_bytes()) else {
414 return;
415 };
416 hostile.insert(name, HeaderValue::from_static("value"));
417 }
418 assert_eq!(
419 inspect_response_head(
420 cloud_sdk::Method::Get,
421 StatusCode::OK,
422 &hostile,
423 policy,
424 &mut captured,
425 16,
426 ),
427 Err(RawHttpError::ResponseHeadTooLarge)
428 );
429 }
430
431 #[test]
432 fn head_and_not_modified_select_zero_body_capacity() {
433 let Some(policy) = policy(&[]) else { return };
434 let mut source = HeaderMap::new();
435 source.insert("content-type", HeaderValue::from_static("application/json"));
436 source.insert("content-length", HeaderValue::from_static("8"));
437 let mut storage = [0_u8; 128];
438 let mut captured = ResponseHeaders::new(&mut storage);
439 assert_eq!(
440 inspect_response_head(
441 cloud_sdk::Method::Head,
442 StatusCode::OK,
443 &source,
444 policy,
445 &mut captured,
446 16,
447 ),
448 Ok(0)
449 );
450 let not_modified = StatusCode::new(304).unwrap_or(StatusCode::NO_CONTENT);
451 assert_eq!(
452 inspect_response_head(
453 cloud_sdk::Method::Get,
454 not_modified,
455 &source,
456 policy,
457 &mut captured,
458 16,
459 ),
460 Ok(0)
461 );
462 }
463}