Skip to main content

foctet_http/
raw.rs

1//! Lower-level HTTP helpers for `application/foctet` envelopes.
2//!
3//! These functions expose direct request/response/body helpers and are useful
4//! when callers need custom assembly.
5//!
6//! The envelope protects HTTP body bytes only. Method, URI, status code, and
7//! outer headers remain visible to the surrounding HTTP transport.
8
9use foctet_core::{
10    BodyEnvelopeLimits, open_body, open_body_with_limits, seal_body, seal_body_with_limits,
11};
12use http::{
13    HeaderMap, Request, Response,
14    header::{self, HeaderName, HeaderValue},
15};
16
17use crate::{CONTENT_TYPE, HttpError};
18
19/// Sets `Content-Type: application/foctet`.
20pub fn set_foctet_content_type(headers: &mut HeaderMap) {
21    headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(CONTENT_TYPE));
22}
23
24/// Sets the advisory Foctet scope header to `body-only`.
25pub fn set_foctet_scope_header(headers: &mut HeaderMap) {
26    headers.insert(
27        HeaderName::from_static(crate::SCOPE_HEADER),
28        HeaderValue::from_static(crate::BODY_ONLY_SCOPE),
29    );
30}
31
32/// Returns `true` if headers contain `Content-Type: application/foctet`.
33///
34/// Parameters after `;` are tolerated.
35pub fn is_foctet_content_type(headers: &HeaderMap) -> bool {
36    headers
37        .get(header::CONTENT_TYPE)
38        .and_then(|value| value.to_str().ok())
39        .is_some_and(is_foctet_content_type_value)
40}
41
42/// Returns `true` if headers contain `x-foctet-scope: body-only`.
43pub fn has_body_only_scope(headers: &HeaderMap) -> bool {
44    headers
45        .get(HeaderName::from_static(crate::SCOPE_HEADER))
46        .and_then(|value| value.to_str().ok())
47        .is_some_and(|value| value.eq_ignore_ascii_case(crate::BODY_ONLY_SCOPE))
48}
49
50/// Validates that headers contain `Content-Type: application/foctet`.
51pub fn ensure_foctet_content_type(headers: &HeaderMap) -> Result<(), HttpError> {
52    let value = headers
53        .get(header::CONTENT_TYPE)
54        .ok_or(HttpError::MissingContentType)?;
55
56    let value = value.to_str().map_err(|_| HttpError::InvalidContentType)?;
57    if is_foctet_content_type_value(value) {
58        return Ok(());
59    }
60
61    Err(HttpError::InvalidContentType)
62}
63
64/// Seals raw plaintext bytes to an `application/foctet` body.
65pub fn seal_http_body(
66    plaintext: &[u8],
67    recipient_public_key: [u8; 32],
68    recipient_key_id: &[u8],
69) -> Result<Vec<u8>, HttpError> {
70    seal_body(plaintext, recipient_public_key, recipient_key_id).map_err(HttpError::SealFailed)
71}
72
73/// Seals raw plaintext bytes to an `application/foctet` body with explicit limits.
74pub fn seal_http_body_with_limits(
75    plaintext: &[u8],
76    recipient_public_key: [u8; 32],
77    recipient_key_id: &[u8],
78    limits: &BodyEnvelopeLimits,
79) -> Result<Vec<u8>, HttpError> {
80    seal_body_with_limits(plaintext, recipient_public_key, recipient_key_id, limits)
81        .map_err(HttpError::SealFailed)
82}
83
84/// Opens an `application/foctet` body to plaintext bytes.
85pub fn open_http_body(
86    envelope: &[u8],
87    recipient_secret_key: [u8; 32],
88) -> Result<Vec<u8>, HttpError> {
89    open_body(envelope, recipient_secret_key).map_err(HttpError::OpenFailed)
90}
91
92/// Opens an `application/foctet` body to plaintext bytes with explicit limits.
93pub fn open_http_body_with_limits(
94    envelope: &[u8],
95    recipient_secret_key: [u8; 32],
96    limits: &BodyEnvelopeLimits,
97) -> Result<Vec<u8>, HttpError> {
98    open_body_with_limits(envelope, recipient_secret_key, limits).map_err(HttpError::OpenFailed)
99}
100
101/// Seals request body and sets `Content-Type: application/foctet`.
102pub fn seal_http_request(
103    request: Request<Vec<u8>>,
104    recipient_public_key: [u8; 32],
105    recipient_key_id: &[u8],
106) -> Result<Request<Vec<u8>>, HttpError> {
107    let sealer = crate::HttpSealer::new(crate::HttpSealOptions::new(
108        recipient_public_key,
109        recipient_key_id,
110    ));
111    sealer.seal_request(request)
112}
113
114/// Seals request body with explicit limits and sets `Content-Type: application/foctet`.
115pub fn seal_http_request_with_limits(
116    request: Request<Vec<u8>>,
117    recipient_public_key: [u8; 32],
118    recipient_key_id: &[u8],
119    limits: &BodyEnvelopeLimits,
120) -> Result<Request<Vec<u8>>, HttpError> {
121    let sealer = crate::HttpSealer::new(
122        crate::HttpSealOptions::new(recipient_public_key, recipient_key_id)
123            .with_limits(limits.clone()),
124    );
125    sealer.seal_request(request)
126}
127
128/// Validates foctet content type and opens request body.
129pub fn open_http_request(
130    request: Request<Vec<u8>>,
131    recipient_secret_key: [u8; 32],
132) -> Result<Request<Vec<u8>>, HttpError> {
133    let opener = crate::HttpOpener::new(crate::HttpOpenOptions::new(recipient_secret_key));
134    opener.open_request(request)
135}
136
137/// Validates foctet content type and opens request body with explicit limits.
138pub fn open_http_request_with_limits(
139    request: Request<Vec<u8>>,
140    recipient_secret_key: [u8; 32],
141    limits: &BodyEnvelopeLimits,
142) -> Result<Request<Vec<u8>>, HttpError> {
143    let opener = crate::HttpOpener::new(
144        crate::HttpOpenOptions::new(recipient_secret_key).with_limits(limits.clone()),
145    );
146    opener.open_request(request)
147}
148
149/// Seals response body and sets `Content-Type: application/foctet`.
150pub fn seal_http_response(
151    response: Response<Vec<u8>>,
152    recipient_public_key: [u8; 32],
153    recipient_key_id: &[u8],
154) -> Result<Response<Vec<u8>>, HttpError> {
155    let sealer = crate::HttpSealer::new(crate::HttpSealOptions::new(
156        recipient_public_key,
157        recipient_key_id,
158    ));
159    sealer.seal_response(response)
160}
161
162/// Seals response body with explicit limits and sets `Content-Type: application/foctet`.
163pub fn seal_http_response_with_limits(
164    response: Response<Vec<u8>>,
165    recipient_public_key: [u8; 32],
166    recipient_key_id: &[u8],
167    limits: &BodyEnvelopeLimits,
168) -> Result<Response<Vec<u8>>, HttpError> {
169    let sealer = crate::HttpSealer::new(
170        crate::HttpSealOptions::new(recipient_public_key, recipient_key_id)
171            .with_limits(limits.clone()),
172    );
173    sealer.seal_response(response)
174}
175
176/// Validates foctet content type and opens response body.
177pub fn open_http_response(
178    response: Response<Vec<u8>>,
179    recipient_secret_key: [u8; 32],
180) -> Result<Response<Vec<u8>>, HttpError> {
181    let opener = crate::HttpOpener::new(crate::HttpOpenOptions::new(recipient_secret_key));
182    opener.open_response(response)
183}
184
185/// Validates foctet content type and opens response body with explicit limits.
186pub fn open_http_response_with_limits(
187    response: Response<Vec<u8>>,
188    recipient_secret_key: [u8; 32],
189    limits: &BodyEnvelopeLimits,
190) -> Result<Response<Vec<u8>>, HttpError> {
191    let opener = crate::HttpOpener::new(
192        crate::HttpOpenOptions::new(recipient_secret_key).with_limits(limits.clone()),
193    );
194    opener.open_response(response)
195}
196
197pub(crate) fn is_foctet_content_type_value(value: &str) -> bool {
198    let media_type = value.split(';').next().unwrap_or_default().trim();
199    media_type.eq_ignore_ascii_case(CONTENT_TYPE)
200}
201
202#[cfg(test)]
203mod tests {
204    use http::{HeaderMap, Request, Response, StatusCode, Version, header};
205    use rand_core::OsRng;
206    use x25519_dalek::{PublicKey, StaticSecret};
207
208    use super::*;
209
210    #[test]
211    fn content_type_helpers_set_and_check() {
212        let mut headers = HeaderMap::new();
213        assert!(!is_foctet_content_type(&headers));
214
215        set_foctet_content_type(&mut headers);
216        assert!(is_foctet_content_type(&headers));
217        assert!(ensure_foctet_content_type(&headers).is_ok());
218    }
219
220    #[test]
221    fn scope_header_helper_sets_body_only_marker() {
222        let mut headers = HeaderMap::new();
223        assert!(!has_body_only_scope(&headers));
224
225        set_foctet_scope_header(&mut headers);
226        assert!(has_body_only_scope(&headers));
227    }
228
229    #[test]
230    fn content_type_helper_accepts_parameters() {
231        let mut headers = HeaderMap::new();
232        headers.insert(
233            header::CONTENT_TYPE,
234            HeaderValue::from_static("application/foctet; charset=binary"),
235        );
236        assert!(is_foctet_content_type(&headers));
237    }
238
239    #[test]
240    fn seal_open_http_body_roundtrip() {
241        let recipient_priv = StaticSecret::random_from_rng(OsRng);
242        let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
243
244        let plain = b"http body bytes";
245        let sealed = seal_http_body(plain, recipient_pub, b"http-kid").expect("seal");
246        let out = open_http_body(&sealed, recipient_priv.to_bytes()).expect("open");
247
248        assert_eq!(out, plain);
249    }
250
251    #[test]
252    fn wrong_content_type_rejected_on_request_open() {
253        let recipient_priv = StaticSecret::random_from_rng(OsRng);
254
255        let req = Request::builder()
256            .uri("https://example.com/upload")
257            .header(header::CONTENT_TYPE, "application/json")
258            .body(Vec::new())
259            .expect("request");
260
261        let err = open_http_request(req, recipient_priv.to_bytes()).expect_err("must fail");
262        assert!(matches!(err, HttpError::InvalidContentType));
263    }
264
265    #[test]
266    fn request_and_response_helpers_roundtrip() {
267        let recipient_priv = StaticSecret::random_from_rng(OsRng);
268        let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
269
270        let request = Request::builder()
271            .method("POST")
272            .uri("https://example.com/submit")
273            .version(Version::HTTP_11)
274            .header("x-trace-id", "abc123")
275            .body(b"request payload".to_vec())
276            .expect("request");
277
278        let sealed_request =
279            seal_http_request(request, recipient_pub, b"kid-rq").expect("seal request");
280        assert!(is_foctet_content_type(sealed_request.headers()));
281
282        let opened_request =
283            open_http_request(sealed_request, recipient_priv.to_bytes()).expect("open request");
284
285        assert_eq!(opened_request.method(), "POST");
286        assert_eq!(opened_request.uri().path(), "/submit");
287        assert_eq!(opened_request.version(), Version::HTTP_11);
288        assert_eq!(opened_request.headers()["x-trace-id"], "abc123");
289        assert!(!opened_request.headers().contains_key(header::CONTENT_TYPE));
290        assert_eq!(opened_request.body(), b"request payload");
291
292        let response = Response::builder()
293            .status(StatusCode::CREATED)
294            .version(Version::HTTP_2)
295            .header("x-server", "foctet")
296            .body(b"response payload".to_vec())
297            .expect("response");
298
299        let sealed_response =
300            seal_http_response(response, recipient_pub, b"kid-rs").expect("seal response");
301        assert!(is_foctet_content_type(sealed_response.headers()));
302
303        let opened_response =
304            open_http_response(sealed_response, recipient_priv.to_bytes()).expect("open response");
305
306        assert_eq!(opened_response.status(), StatusCode::CREATED);
307        assert_eq!(opened_response.version(), Version::HTTP_2);
308        assert_eq!(opened_response.headers()["x-server"], "foctet");
309        assert!(!opened_response.headers().contains_key(header::CONTENT_TYPE));
310        assert_eq!(opened_response.body(), b"response payload");
311    }
312
313    #[test]
314    fn with_limits_passthrough_behaves_as_expected() {
315        let recipient_priv = StaticSecret::random_from_rng(OsRng);
316        let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
317        let limits = BodyEnvelopeLimits {
318            max_payload_len: 1024,
319            ..BodyEnvelopeLimits::default()
320        };
321
322        let sealed = seal_http_body_with_limits(b"hello", recipient_pub, b"kid", &limits)
323            .expect("seal with limits");
324        let opened =
325            open_http_body_with_limits(&sealed, recipient_priv.to_bytes(), &limits).expect("open");
326        assert_eq!(opened, b"hello");
327    }
328}