1use 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
19pub fn set_foctet_content_type(headers: &mut HeaderMap) {
21 headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(CONTENT_TYPE));
22}
23
24pub 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
32pub 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
42pub 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
50pub 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
64pub 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
73pub 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
84pub 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
92pub 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
101pub 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
114pub 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
128pub 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
137pub 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
149pub 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
162pub 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
176pub 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
185pub 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}