cloud-sdk-reqwest 0.26.0

Optional provider-neutral reqwest transport boundary for cloud-sdk.
Documentation
use cloud_sdk::transport::{HeaderSensitivity, ResponseHeaders};
use reqwest::header::{HeaderMap, HeaderValue};

use super::TransportError;

const REVIEWED_PUBLIC_RESPONSE_HEADERS: &[&str] = &[
    "content-length",
    "content-type",
    "date",
    "ratelimit-limit",
    "ratelimit-remaining",
    "ratelimit-reset",
];

pub(crate) fn capture_response_headers(
    source: &HeaderMap,
    captured: &mut ResponseHeaders<'_>,
) -> Result<(), TransportError> {
    for (name, value) in source {
        let sensitivity = response_sensitivity(name.as_str(), value);
        captured
            .try_push(name.as_str(), value.as_bytes(), sensitivity)
            .map_err(|_| TransportError::InvalidResponseHeaders)?;
    }
    Ok(())
}

fn response_sensitivity(name: &str, value: &HeaderValue) -> HeaderSensitivity {
    if value.is_sensitive()
        || !REVIEWED_PUBLIC_RESPONSE_HEADERS
            .iter()
            .any(|candidate| name.eq_ignore_ascii_case(candidate))
    {
        HeaderSensitivity::Sensitive
    } else {
        HeaderSensitivity::Public
    }
}

#[cfg(test)]
mod tests {
    use reqwest::header::{HeaderMap, HeaderValue};

    use super::capture_response_headers;
    use crate::shared::TransportError;
    use cloud_sdk::transport::HeaderSensitivity;

    #[test]
    fn defaults_unknown_metadata_to_sensitive() {
        let mut source = HeaderMap::new();
        source.insert("x-request-id", HeaderValue::from_static("abc"));
        source.insert("x-api-key", HeaderValue::from_static("secret-key"));
        source.insert("authorization", HeaderValue::from_static("Bearer secret"));
        source.insert("set-cookie", HeaderValue::from_static("secret=1"));
        let mut storage = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
        let mut headers = cloud_sdk::transport::ResponseHeaders::new(&mut storage);
        let captured = capture_response_headers(&source, &mut headers);
        assert!(captured.is_ok());
        if captured.is_ok() {
            assert_eq!(headers.len(), 4);
            for name in ["x-request-id", "x-api-key", "authorization", "set-cookie"] {
                assert_eq!(
                    headers.get(name).map(|header| header.sensitivity()),
                    Some(HeaderSensitivity::Sensitive)
                );
            }
        }
    }

    #[test]
    fn classifies_only_reviewed_metadata_as_public() {
        for name in super::REVIEWED_PUBLIC_RESPONSE_HEADERS {
            let mut source = HeaderMap::new();
            source.insert(*name, HeaderValue::from_static("1"));
            let mut storage = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
            let mut headers = cloud_sdk::transport::ResponseHeaders::new(&mut storage);
            let captured = capture_response_headers(&source, &mut headers);
            assert!(captured.is_ok());
            if captured.is_ok() {
                assert_eq!(
                    headers.get(name).map(|header| header.sensitivity()),
                    Some(HeaderSensitivity::Public)
                );
            }
        }
    }

    #[test]
    fn preserves_explicit_sensitivity_on_reviewed_public_metadata() {
        let mut source = HeaderMap::new();
        let mut value = HeaderValue::from_static("application/json");
        value.set_sensitive(true);
        source.insert("content-type", value);

        let mut storage = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
        let mut headers = cloud_sdk::transport::ResponseHeaders::new(&mut storage);
        let captured = capture_response_headers(&source, &mut headers);
        assert!(captured.is_ok());
        if captured.is_ok() {
            assert_eq!(
                headers
                    .get("content-type")
                    .map(|header| header.sensitivity()),
                Some(HeaderSensitivity::Sensitive)
            );
        }
    }

    #[test]
    fn rejects_identical_and_conflicting_duplicates() {
        for duplicate in ["same", "different"] {
            let mut source = HeaderMap::new();
            source.append("x-test", HeaderValue::from_static("same"));
            source.append(
                "x-test",
                HeaderValue::from_str(duplicate).unwrap_or(HeaderValue::from_static("invalid")),
            );
            let mut storage = [0_u8; cloud_sdk::transport::MAX_RESPONSE_HEADER_BYTES];
            let mut headers = cloud_sdk::transport::ResponseHeaders::new(&mut storage);
            assert!(matches!(
                capture_response_headers(&source, &mut headers),
                Err(TransportError::InvalidResponseHeaders)
            ));
        }
    }
}