1pub use http;
28
29mod config;
30mod error;
31pub mod raw;
32
33#[cfg(feature = "axum")]
34pub mod axum;
35#[cfg(all(feature = "workers", target_arch = "wasm32"))]
36pub mod workers;
37
38use http::{
39 Request, Response,
40 header::{self},
41};
42
43pub use config::{HttpConfig, HttpOpenOptions, HttpSealOptions};
44pub use error::HttpError;
45
46pub const CONTENT_TYPE: &str = "application/foctet";
48pub const SCOPE_HEADER: &str = "x-foctet-scope";
50pub const BODY_ONLY_SCOPE: &str = "body-only";
52
53#[derive(Clone, Debug)]
55pub struct HttpSealer {
56 options: HttpSealOptions,
57 config: HttpConfig,
58}
59
60#[derive(Clone, Debug)]
62pub struct HttpOpener {
63 options: HttpOpenOptions,
64 config: HttpConfig,
65}
66
67impl HttpSealer {
68 pub fn new(options: HttpSealOptions) -> Self {
70 Self {
71 options,
72 config: HttpConfig::default(),
73 }
74 }
75
76 pub fn with_config(options: HttpSealOptions, config: HttpConfig) -> Self {
78 Self { options, config }
79 }
80
81 pub fn options(&self) -> &HttpSealOptions {
83 &self.options
84 }
85
86 pub fn config(&self) -> &HttpConfig {
88 &self.config
89 }
90
91 pub fn seal_body(&self, plaintext: &[u8]) -> Result<Vec<u8>, HttpError> {
93 match self.options.limits() {
94 Some(limits) => raw::seal_http_body_with_limits(
95 plaintext,
96 self.options.recipient_public_key(),
97 self.options.recipient_key_id(),
98 limits,
99 ),
100 None => raw::seal_http_body(
101 plaintext,
102 self.options.recipient_public_key(),
103 self.options.recipient_key_id(),
104 ),
105 }
106 }
107
108 pub fn seal_request(&self, request: Request<Vec<u8>>) -> Result<Request<Vec<u8>>, HttpError> {
114 let (mut parts, body) = request.into_parts();
115 let sealed = self.seal_body(&body)?;
116 raw::set_foctet_content_type(&mut parts.headers);
117 if self.config.set_scope_header_on_seal() {
118 raw::set_foctet_scope_header(&mut parts.headers);
119 }
120 Ok(Request::from_parts(parts, sealed))
121 }
122
123 pub fn seal_response(
129 &self,
130 response: Response<Vec<u8>>,
131 ) -> Result<Response<Vec<u8>>, HttpError> {
132 let (mut parts, body) = response.into_parts();
133 let sealed = self.seal_body(&body)?;
134 raw::set_foctet_content_type(&mut parts.headers);
135 if self.config.set_scope_header_on_seal() {
136 raw::set_foctet_scope_header(&mut parts.headers);
137 }
138 Ok(Response::from_parts(parts, sealed))
139 }
140}
141
142impl HttpOpener {
143 pub fn new(options: HttpOpenOptions) -> Self {
145 Self {
146 options,
147 config: HttpConfig::default(),
148 }
149 }
150
151 pub fn with_config(options: HttpOpenOptions, config: HttpConfig) -> Self {
153 Self { options, config }
154 }
155
156 pub fn options(&self) -> &HttpOpenOptions {
158 &self.options
159 }
160
161 pub fn config(&self) -> &HttpConfig {
163 &self.config
164 }
165
166 pub fn open_body(&self, envelope: &[u8]) -> Result<Vec<u8>, HttpError> {
168 match self.options.limits() {
169 Some(limits) => raw::open_http_body_with_limits(
170 envelope,
171 self.options.recipient_secret_key(),
172 limits,
173 ),
174 None => raw::open_http_body(envelope, self.options.recipient_secret_key()),
175 }
176 }
177
178 pub fn open_request(&self, request: Request<Vec<u8>>) -> Result<Request<Vec<u8>>, HttpError> {
180 let (mut parts, body) = request.into_parts();
181 raw::ensure_foctet_content_type(&parts.headers)?;
182 let plain = self.open_body(&body)?;
183 if self.config.strip_content_type_on_open() {
184 parts.headers.remove(header::CONTENT_TYPE);
185 }
186 Ok(Request::from_parts(parts, plain))
187 }
188
189 pub fn open_response(
191 &self,
192 response: Response<Vec<u8>>,
193 ) -> Result<Response<Vec<u8>>, HttpError> {
194 let (mut parts, body) = response.into_parts();
195 raw::ensure_foctet_content_type(&parts.headers)?;
196 let plain = self.open_body(&body)?;
197 if self.config.strip_content_type_on_open() {
198 parts.headers.remove(header::CONTENT_TYPE);
199 }
200 Ok(Response::from_parts(parts, plain))
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use foctet_core::BodyEnvelopeLimits;
207 use http::{Request, Response, StatusCode, Version, header};
208 use rand_core::OsRng;
209 use x25519_dalek::{PublicKey, StaticSecret};
210
211 use super::*;
212
213 #[test]
214 fn sealer_and_opener_roundtrip_request_and_response() {
215 let recipient_priv = StaticSecret::random_from_rng(OsRng);
216 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
217
218 let sealer = HttpSealer::new(HttpSealOptions::new(recipient_pub, b"kid"));
219 let opener = HttpOpener::new(HttpOpenOptions::new(recipient_priv.to_bytes()));
220
221 let request = Request::builder()
222 .method("POST")
223 .uri("https://example.com/submit")
224 .version(Version::HTTP_11)
225 .header("x-trace-id", "abc123")
226 .body(b"request payload".to_vec())
227 .expect("request");
228
229 let sealed_request = sealer.seal_request(request).expect("seal request");
230 assert_eq!(sealed_request.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
231 let opened_request = opener.open_request(sealed_request).expect("open request");
232
233 assert_eq!(opened_request.method(), "POST");
234 assert_eq!(opened_request.uri().path(), "/submit");
235 assert_eq!(opened_request.headers()["x-trace-id"], "abc123");
236 assert_eq!(opened_request.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
237 assert!(!opened_request.headers().contains_key(header::CONTENT_TYPE));
238 assert_eq!(opened_request.body(), b"request payload");
239
240 let response = Response::builder()
241 .status(StatusCode::CREATED)
242 .version(Version::HTTP_2)
243 .header("x-server", "foctet")
244 .body(b"response payload".to_vec())
245 .expect("response");
246
247 let sealed_response = sealer.seal_response(response).expect("seal response");
248 assert_eq!(sealed_response.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
249 let opened_response = opener
250 .open_response(sealed_response)
251 .expect("open response");
252
253 assert_eq!(opened_response.status(), StatusCode::CREATED);
254 assert_eq!(opened_response.version(), Version::HTTP_2);
255 assert_eq!(opened_response.headers()["x-server"], "foctet");
256 assert_eq!(opened_response.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
257 assert_eq!(opened_response.body(), b"response payload");
258 }
259
260 #[test]
261 fn opener_respects_explicit_config() {
262 let recipient_priv = StaticSecret::random_from_rng(OsRng);
263 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
264
265 let sealer = HttpSealer::new(HttpSealOptions::new(recipient_pub, b"kid"));
266 let opener = HttpOpener::with_config(
267 HttpOpenOptions::new(recipient_priv.to_bytes()),
268 HttpConfig::default().with_strip_content_type_on_open(false),
269 );
270
271 let response = Response::builder()
272 .status(StatusCode::OK)
273 .body(b"payload".to_vec())
274 .expect("response");
275 let sealed = sealer.seal_response(response).expect("seal");
276 let opened = opener.open_response(sealed).expect("open");
277
278 assert!(opened.headers().contains_key(header::CONTENT_TYPE));
279 assert_eq!(opened.headers()[SCOPE_HEADER], BODY_ONLY_SCOPE);
280 }
281
282 #[test]
283 fn scope_header_can_be_disabled() {
284 let recipient_priv = StaticSecret::random_from_rng(OsRng);
285 let recipient_pub = PublicKey::from(&recipient_priv).to_bytes();
286
287 let sealer = HttpSealer::with_config(
288 HttpSealOptions::new(recipient_pub, b"kid"),
289 HttpConfig::default().with_scope_header_on_seal(false),
290 );
291
292 let response = Response::builder()
293 .status(StatusCode::OK)
294 .body(b"payload".to_vec())
295 .expect("response");
296 let sealed = sealer.seal_response(response).expect("seal");
297
298 assert!(!sealed.headers().contains_key(SCOPE_HEADER));
299 }
300
301 #[test]
302 fn options_support_explicit_limits() {
303 let limits = BodyEnvelopeLimits {
304 max_payload_len: 1024,
305 ..BodyEnvelopeLimits::default()
306 };
307 let options = HttpSealOptions::new([1u8; 32], b"kid").with_limits(limits.clone());
308 assert_eq!(options.limits(), Some(&limits));
309 }
310}