pub use http;
mod config;
mod error;
pub mod raw;
#[cfg(feature = "axum")]
pub mod axum;
#[cfg(all(feature = "workers", target_arch = "wasm32"))]
pub mod workers;
use http::{
Request, Response,
header::{self},
};
pub use config::{HttpConfig, HttpOpenOptions, HttpSealOptions};
pub use error::HttpError;
pub const CONTENT_TYPE: &str = "application/foctet";
pub const SCOPE_HEADER: &str = "x-foctet-scope";
pub const BODY_ONLY_SCOPE: &str = "body-only";
#[derive(Clone, Debug)]
pub struct HttpSealer {
options: HttpSealOptions,
config: HttpConfig,
}
#[derive(Clone, Debug)]
pub struct HttpOpener {
options: HttpOpenOptions,
config: HttpConfig,
}
impl HttpSealer {
pub fn new(options: HttpSealOptions) -> Self {
Self {
options,
config: HttpConfig::default(),
}
}
pub fn with_config(options: HttpSealOptions, config: HttpConfig) -> Self {
Self { options, config }
}
pub fn options(&self) -> &HttpSealOptions {
&self.options
}
pub fn config(&self) -> &HttpConfig {
&self.config
}
pub fn seal_body(&self, plaintext: &[u8]) -> Result<Vec<u8>, HttpError> {
match self.options.limits() {
Some(limits) => raw::seal_http_body_with_limits(
plaintext,
self.options.recipient_public_key(),
self.options.recipient_key_id(),
limits,
),
None => raw::seal_http_body(
plaintext,
self.options.recipient_public_key(),
self.options.recipient_key_id(),
),
}
}
pub fn seal_request(&self, request: Request<Vec<u8>>) -> Result<Request<Vec<u8>>, HttpError> {
let (mut parts, body) = request.into_parts();
let sealed = self.seal_body(&body)?;
raw::set_foctet_content_type(&mut parts.headers);
if self.config.set_scope_header_on_seal() {
raw::set_foctet_scope_header(&mut parts.headers);
}
Ok(Request::from_parts(parts, sealed))
}
pub fn seal_response(
&self,
response: Response<Vec<u8>>,
) -> Result<Response<Vec<u8>>, HttpError> {
let (mut parts, body) = response.into_parts();
let sealed = self.seal_body(&body)?;
raw::set_foctet_content_type(&mut parts.headers);
if self.config.set_scope_header_on_seal() {
raw::set_foctet_scope_header(&mut parts.headers);
}
Ok(Response::from_parts(parts, sealed))
}
}
impl HttpOpener {
pub fn new(options: HttpOpenOptions) -> Self {
Self {
options,
config: HttpConfig::default(),
}
}
pub fn with_config(options: HttpOpenOptions, config: HttpConfig) -> Self {
Self { options, config }
}
pub fn options(&self) -> &HttpOpenOptions {
&self.options
}
pub fn config(&self) -> &HttpConfig {
&self.config
}
pub fn open_body(&self, envelope: &[u8]) -> Result<Vec<u8>, HttpError> {
match self.options.limits() {
Some(limits) => raw::open_http_body_with_limits(
envelope,
self.options.recipient_secret_key(),
limits,
),
None => raw::open_http_body(envelope, self.options.recipient_secret_key()),
}
}
pub fn open_request(&self, request: Request<Vec<u8>>) -> Result<Request<Vec<u8>>, HttpError> {
let (mut parts, body) = request.into_parts();
raw::ensure_foctet_content_type(&parts.headers)?;
let plain = self.open_body(&body)?;
if self.config.strip_content_type_on_open() {
parts.headers.remove(header::CONTENT_TYPE);
}
Ok(Request::from_parts(parts, plain))
}
pub fn open_response(
&self,
response: Response<Vec<u8>>,
) -> Result<Response<Vec<u8>>, HttpError> {
let (mut parts, body) = response.into_parts();
raw::ensure_foctet_content_type(&parts.headers)?;
let plain = self.open_body(&body)?;
if self.config.strip_content_type_on_open() {
parts.headers.remove(header::CONTENT_TYPE);
}
Ok(Response::from_parts(parts, plain))
}
}
#[cfg(test)]
mod tests {
use foctet_core::BodyEnvelopeLimits;
use http::{Request, Response, StatusCode, Version, header};
use rand_core::OsRng;
use x25519_dalek::{PublicKey, StaticSecret};
use super::*;
#[test]
fn sealer_and_opener_roundtrip_request_and_response() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
let sealer = HttpSealer::new(HttpSealOptions::new(recipient_pub, b"kid"));
let opener = HttpOpener::new(HttpOpenOptions::new(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 = sealer.seal_request(request).expect("seal request");
assert_eq!(sealed_request.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
let opened_request = opener.open_request(sealed_request).expect("open request");
assert_eq!(opened_request.method(), "POST");
assert_eq!(opened_request.uri().path(), "/submit");
assert_eq!(opened_request.headers()["x-trace-id"], "abc123");
assert_eq!(opened_request.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
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 = sealer.seal_response(response).expect("seal response");
assert_eq!(sealed_response.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
let opened_response = opener
.open_response(sealed_response)
.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_eq!(opened_response.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
assert_eq!(opened_response.body(), b"response payload");
}
#[test]
fn opener_respects_explicit_config() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
let sealer = HttpSealer::new(HttpSealOptions::new(recipient_pub, b"kid"));
let opener = HttpOpener::with_config(
HttpOpenOptions::new(recipient_priv.to_bytes()),
HttpConfig::default().with_strip_content_type_on_open(false),
);
let response = Response::builder()
.status(StatusCode::OK)
.body(b"payload".to_vec())
.expect("response");
let sealed = sealer.seal_response(response).expect("seal");
let opened = opener.open_response(sealed).expect("open");
assert!(opened.headers().contains_key(header::CONTENT_TYPE));
assert_eq!(opened.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
}
#[test]
fn scope_header_can_be_disabled() {
let recipient_priv = StaticSecret::random_from_rng(OsRng);
let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
let sealer = HttpSealer::with_config(
HttpSealOptions::new(recipient_pub, b"kid"),
HttpConfig::default().with_scope_header_on_seal(false),
);
let response = Response::builder()
.status(StatusCode::OK)
.body(b"payload".to_vec())
.expect("response");
let sealed = sealer.seal_response(response).expect("seal");
assert!(!sealed.headers().contains_key(SCOPE_HEADER));
}
#[test]
fn options_support_explicit_limits() {
let limits = BodyEnvelopeLimits {
max_payload_len: 1024,
..BodyEnvelopeLimits::default()
};
let options = HttpSealOptions::new([1u8; 32], b"kid").with_limits(limits.clone());
assert_eq!(options.limits(), Some(&limits));
}
}