use foctet_core::{
BodyEnvelopeLimits, open_body, open_body_with_limits, seal_body, seal_body_with_limits,
};
use http::{
HeaderMap, Request, Response,
header::{self, HeaderName, HeaderValue},
};
use crate::{CONTENT_TYPE, HttpError};
pub fn set_foctet_content_type(headers: &mut HeaderMap) {
headers.insert(header::CONTENT_TYPE, HeaderValue::from_static(CONTENT_TYPE));
}
pub fn set_foctet_scope_header(headers: &mut HeaderMap) {
headers.insert(
HeaderName::from_static(crate::SCOPE_HEADER),
HeaderValue::from_static(crate::BODY_ONLY_SCOPE),
);
}
pub fn is_foctet_content_type(headers: &HeaderMap) -> bool {
headers
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(is_foctet_content_type_value)
}
pub fn has_body_only_scope(headers: &HeaderMap) -> bool {
headers
.get(HeaderName::from_static(crate::SCOPE_HEADER))
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.eq_ignore_ascii_case(crate::BODY_ONLY_SCOPE))
}
pub fn ensure_foctet_content_type(headers: &HeaderMap) -> Result<(), HttpError> {
let value = headers
.get(header::CONTENT_TYPE)
.ok_or(HttpError::MissingContentType)?;
let value = value.to_str().map_err(|_| HttpError::InvalidContentType)?;
if is_foctet_content_type_value(value) {
return Ok(());
}
Err(HttpError::InvalidContentType)
}
pub fn seal_http_body(
plaintext: &[u8],
recipient_public_key: [u8; 32],
recipient_key_id: &[u8],
) -> Result<Vec<u8>, HttpError> {
seal_body(plaintext, recipient_public_key, recipient_key_id).map_err(HttpError::SealFailed)
}
pub fn seal_http_body_with_limits(
plaintext: &[u8],
recipient_public_key: [u8; 32],
recipient_key_id: &[u8],
limits: &BodyEnvelopeLimits,
) -> Result<Vec<u8>, HttpError> {
seal_body_with_limits(plaintext, recipient_public_key, recipient_key_id, limits)
.map_err(HttpError::SealFailed)
}
pub fn open_http_body(
envelope: &[u8],
recipient_secret_key: [u8; 32],
) -> Result<Vec<u8>, HttpError> {
open_body(envelope, recipient_secret_key).map_err(HttpError::OpenFailed)
}
pub fn open_http_body_with_limits(
envelope: &[u8],
recipient_secret_key: [u8; 32],
limits: &BodyEnvelopeLimits,
) -> Result<Vec<u8>, HttpError> {
open_body_with_limits(envelope, recipient_secret_key, limits).map_err(HttpError::OpenFailed)
}
pub fn seal_http_request(
request: Request<Vec<u8>>,
recipient_public_key: [u8; 32],
recipient_key_id: &[u8],
) -> Result<Request<Vec<u8>>, HttpError> {
let sealer = crate::HttpSealer::new(crate::HttpSealOptions::new(
recipient_public_key,
recipient_key_id,
));
sealer.seal_request(request)
}
pub fn seal_http_request_with_limits(
request: Request<Vec<u8>>,
recipient_public_key: [u8; 32],
recipient_key_id: &[u8],
limits: &BodyEnvelopeLimits,
) -> Result<Request<Vec<u8>>, HttpError> {
let sealer = crate::HttpSealer::new(
crate::HttpSealOptions::new(recipient_public_key, recipient_key_id)
.with_limits(limits.clone()),
);
sealer.seal_request(request)
}
pub fn open_http_request(
request: Request<Vec<u8>>,
recipient_secret_key: [u8; 32],
) -> Result<Request<Vec<u8>>, HttpError> {
let opener = crate::HttpOpener::new(crate::HttpOpenOptions::new(recipient_secret_key));
opener.open_request(request)
}
pub fn open_http_request_with_limits(
request: Request<Vec<u8>>,
recipient_secret_key: [u8; 32],
limits: &BodyEnvelopeLimits,
) -> Result<Request<Vec<u8>>, HttpError> {
let opener = crate::HttpOpener::new(
crate::HttpOpenOptions::new(recipient_secret_key).with_limits(limits.clone()),
);
opener.open_request(request)
}
pub fn seal_http_response(
response: Response<Vec<u8>>,
recipient_public_key: [u8; 32],
recipient_key_id: &[u8],
) -> Result<Response<Vec<u8>>, HttpError> {
let sealer = crate::HttpSealer::new(crate::HttpSealOptions::new(
recipient_public_key,
recipient_key_id,
));
sealer.seal_response(response)
}
pub fn seal_http_response_with_limits(
response: Response<Vec<u8>>,
recipient_public_key: [u8; 32],
recipient_key_id: &[u8],
limits: &BodyEnvelopeLimits,
) -> Result<Response<Vec<u8>>, HttpError> {
let sealer = crate::HttpSealer::new(
crate::HttpSealOptions::new(recipient_public_key, recipient_key_id)
.with_limits(limits.clone()),
);
sealer.seal_response(response)
}
pub fn open_http_response(
response: Response<Vec<u8>>,
recipient_secret_key: [u8; 32],
) -> Result<Response<Vec<u8>>, HttpError> {
let opener = crate::HttpOpener::new(crate::HttpOpenOptions::new(recipient_secret_key));
opener.open_response(response)
}
pub fn open_http_response_with_limits(
response: Response<Vec<u8>>,
recipient_secret_key: [u8; 32],
limits: &BodyEnvelopeLimits,
) -> Result<Response<Vec<u8>>, HttpError> {
let opener = crate::HttpOpener::new(
crate::HttpOpenOptions::new(recipient_secret_key).with_limits(limits.clone()),
);
opener.open_response(response)
}
pub(crate) fn is_foctet_content_type_value(value: &str) -> bool {
let media_type = value.split(';').next().unwrap_or_default().trim();
media_type.eq_ignore_ascii_case(CONTENT_TYPE)
}
#[cfg(test)]
mod tests {
use http::{HeaderMap, Request, Response, StatusCode, Version, header};
use rand_core::OsRng;
use x25519_dalek::{PublicKey, StaticSecret};
use super::*;
#[test]
fn content_type_helpers_set_and_check() {
let mut headers = HeaderMap::new();
assert!(!is_foctet_content_type(&headers));
set_foctet_content_type(&mut headers);
assert!(is_foctet_content_type(&headers));
assert!(ensure_foctet_content_type(&headers).is_ok());
}
#[test]
fn scope_header_helper_sets_body_only_marker() {
let mut headers = HeaderMap::new();
assert!(!has_body_only_scope(&headers));
set_foctet_scope_header(&mut headers);
assert!(has_body_only_scope(&headers));
}
#[test]
fn content_type_helper_accepts_parameters() {
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/foctet; charset=binary"),
);
assert!(is_foctet_content_type(&headers));
}
#[test]
fn seal_open_http_body_roundtrip() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
let plain = b"http body bytes";
let sealed = seal_http_body(plain, recipient_pub, b"http-kid").expect("seal");
let out = open_http_body(&sealed, recipient_priv.to_bytes()).expect("open");
assert_eq!(out, plain);
}
#[test]
fn wrong_content_type_rejected_on_request_open() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let req = Request::builder()
.uri("https://example.com/upload")
.header(header::CONTENT_TYPE, "application/json")
.body(Vec::new())
.expect("request");
let err = open_http_request(req, recipient_priv.to_bytes()).expect_err("must fail");
assert!(matches!(err, HttpError::InvalidContentType));
}
#[test]
fn request_and_response_helpers_roundtrip() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
let request = Request::builder()
.method("POST")
.uri("https://example.com/submit")
.version(Version::HTTP_11)
.header("x-trace-id", "abc123")
.body(b"request payload".to_vec())
.expect("request");
let sealed_request =
seal_http_request(request, recipient_pub, b"kid-rq").expect("seal request");
assert!(is_foctet_content_type(sealed_request.headers()));
let opened_request =
open_http_request(sealed_request, recipient_priv.to_bytes()).expect("open request");
assert_eq!(opened_request.method(), "POST");
assert_eq!(opened_request.uri().path(), "/submit");
assert_eq!(opened_request.version(), Version::HTTP_11);
assert_eq!(opened_request.headers()["x-trace-id"], "abc123");
assert!(!opened_request.headers().contains_key(header::CONTENT_TYPE));
assert_eq!(opened_request.body(), b"request payload");
let response = Response::builder()
.status(StatusCode::CREATED)
.version(Version::HTTP_2)
.header("x-server", "foctet")
.body(b"response payload".to_vec())
.expect("response");
let sealed_response =
seal_http_response(response, recipient_pub, b"kid-rs").expect("seal response");
assert!(is_foctet_content_type(sealed_response.headers()));
let opened_response =
open_http_response(sealed_response, recipient_priv.to_bytes()).expect("open response");
assert_eq!(opened_response.status(), StatusCode::CREATED);
assert_eq!(opened_response.version(), Version::HTTP_2);
assert_eq!(opened_response.headers()["x-server"], "foctet");
assert!(!opened_response.headers().contains_key(header::CONTENT_TYPE));
assert_eq!(opened_response.body(), b"response payload");
}
#[test]
fn with_limits_passthrough_behaves_as_expected() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
let limits = BodyEnvelopeLimits {
max_payload_len: 1024,
..BodyEnvelopeLimits::default()
};
let sealed = seal_http_body_with_limits(b"hello", recipient_pub, b"kid", &limits)
.expect("seal with limits");
let opened =
open_http_body_with_limits(&sealed, recipient_priv.to_bytes(), &limits).expect("open");
assert_eq!(opened, b"hello");
}
}