1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use std::borrow::Cow;
5use std::future;
6use std::net::{SocketAddr, ToSocketAddrs};
7use std::str::FromStr;
8use std::sync::Arc;
9
10use anyhow::anyhow;
11use bytes::Bytes;
12use http::Extensions;
13use http::header::{HeaderName, HeaderValue};
14use http_acl::utils::authority::{Authority, Host};
15use reqwest::{
16 Body, Request, Response, ResponseBuilderExt,
17 dns::{Name, Resolve, Resolving},
18 redirect,
19};
20use reqwest_middleware::{Error, Middleware, Next};
21use thiserror::Error;
22
23pub use http_acl::{
24 self, HttpAcl, HttpAclBuilder, HttpAclHooks, ModifyRequestFn, ModifyResponseFn,
25 RequestMutation, ResponseMutation, ValidateFn,
26};
27
28#[derive(Debug, Clone)]
29pub struct HttpAclMiddleware {
54 acl: Arc<HttpAcl>,
55}
56
57impl HttpAclMiddleware {
58 pub fn new(acl: HttpAcl) -> Self {
60 Self { acl: Arc::new(acl) }
61 }
62
63 pub fn acl(&self) -> Arc<HttpAcl> {
65 self.acl.clone()
66 }
67
68 pub fn dns_resolver(&self) -> Arc<HttpAclDnsResolver> {
76 Arc::new(HttpAclDnsResolver::new(self))
77 }
78
79 pub fn with_dns_resolver(&self, dns_resolver: Arc<dyn Resolve>) -> Arc<HttpAclDnsResolver> {
82 Arc::new(HttpAclDnsResolver::with_dns_resolver(self, dns_resolver))
83 }
84
85 pub fn redirect_policy(&self) -> redirect::Policy {
126 self.redirect_policy_with_max(10)
127 }
128
129 pub fn redirect_policy_with_max(&self, max_redirects: usize) -> redirect::Policy {
131 let acl = self.acl.clone();
132 redirect::Policy::custom(move |attempt| {
133 let deny_reason = 'reason: {
137 if attempt.previous().len() > max_redirects {
138 break 'reason Some("too many redirects".to_string());
139 }
140
141 let url = attempt.url();
142
143 let scheme = url.scheme();
144 if acl.is_scheme_allowed(scheme).is_denied() {
145 break 'reason Some(format!("scheme {scheme} is denied"));
146 }
147
148 let Some(host) = url.host() else {
149 break 'reason Some("missing host".to_string());
150 };
151
152 match host {
153 url::Host::Domain(domain) => {
154 if acl.is_host_allowed(domain).is_denied() {
155 break 'reason Some(format!("host {domain} is denied"));
156 }
157 }
158 url::Host::Ipv4(ip) => {
159 let ip = std::net::IpAddr::V4(ip);
160 if acl.is_ip_allowed(&ip).is_denied() {
161 break 'reason Some(format!("ip {ip} is denied"));
162 }
163 }
164 url::Host::Ipv6(ip) => {
165 let ip = std::net::IpAddr::V6(ip);
166 if acl.is_ip_allowed(&ip).is_denied() {
167 break 'reason Some(format!("ip {ip} is denied"));
168 }
169 }
170 }
171
172 if let Some(port) = url.port_or_known_default()
173 && acl.is_port_allowed(port).is_denied()
174 {
175 break 'reason Some(format!("port {port} is denied"));
176 }
177
178 match percent_encoding::percent_decode_str(url.path()).decode_utf8() {
181 Ok(path) => {
182 if acl.is_url_path_allowed(&path).is_denied() {
183 break 'reason Some(format!("path {path} is denied"));
184 }
185 }
186 Err(_) => break 'reason Some("invalid URL path encoding".to_string()),
187 }
188
189 None
190 };
191
192 match deny_reason {
193 Some(reason) => attempt.error(std::io::Error::other(reason)),
194 None => attempt.follow(),
195 }
196 })
197 }
198}
199
200#[async_trait::async_trait]
201impl Middleware for HttpAclMiddleware {
202 async fn handle(
203 &self,
204 mut req: Request,
205 extensions: &mut Extensions,
206 next: Next<'_>,
207 ) -> std::result::Result<Response, Error> {
208 let scheme = req.url().scheme().to_string();
212 let acl_scheme_match = self.acl.is_scheme_allowed(&scheme);
213 if acl_scheme_match.is_denied() {
214 return Err(Error::Middleware(anyhow!(
215 "scheme {} is denied - {}",
216 scheme,
217 acl_scheme_match
218 )));
219 }
220
221 let method = req.method().as_str();
222 let acl_method_match = self.acl.is_method_allowed(method);
223 if acl_method_match.is_denied() {
224 return Err(Error::Middleware(anyhow!(
225 "method {} is denied - {}",
226 method,
227 acl_method_match
228 )));
229 }
230
231 if let Some(host) = req.url().host_str() {
232 let authority = Authority::parse(host)
233 .map_err(|_| Error::Middleware(anyhow!("invalid host: {}", host)))?;
234
235 match &authority.host {
236 Host::Ip(ip) => {
237 let acl_ip_match = self.acl.is_ip_allowed(ip);
238 if acl_ip_match.is_denied() {
239 return Err(Error::Middleware(anyhow!(
240 "ip {} is denied - {}",
241 ip,
242 acl_ip_match
243 )));
244 }
245 }
246 Host::Domain(domain) => {
247 let acl_host_match = self.acl.is_host_allowed(domain);
248 if acl_host_match.is_denied() {
249 return Err(Error::Middleware(anyhow!(
250 "host {} is denied - {}",
251 domain,
252 acl_host_match
253 )));
254 }
255 }
256 }
257
258 if let Some(port) = req.url().port_or_known_default() {
259 let acl_port_match = self.acl.is_port_allowed(port);
260 if acl_port_match.is_denied() {
261 return Err(Error::Middleware(anyhow!(
262 "port {} is denied - {}",
263 port,
264 acl_port_match
265 )));
266 }
267 }
268
269 for (key, value) in req.headers() {
272 let header_name = key.as_str();
273 let acl_header_match = self
274 .acl
275 .is_header_allowed_bytes(header_name, value.as_bytes());
276 if acl_header_match.is_denied() {
277 return Err(Error::Middleware(anyhow!(
278 "header {}: {} is denied - {}",
279 header_name,
280 header_value_lossy(value),
281 acl_header_match
282 )));
283 }
284 }
285
286 let url_path = percent_encoding::percent_decode_str(req.url().path())
289 .decode_utf8()
290 .map_err(|_| Error::Middleware(anyhow!("invalid URL path encoding")))?;
291 let acl_url_path_match = self.acl.is_url_path_allowed(&url_path);
292 if acl_url_path_match.is_denied() {
293 return Err(Error::Middleware(anyhow!(
294 "path {} is denied - {}",
295 url_path,
296 acl_url_path_match
297 )));
298 }
299
300 let lossy_headers: Vec<(&str, Cow<'_, str>)> = req
304 .headers()
305 .iter()
306 .map(|(k, v)| (k.as_str(), header_value_lossy(v)))
307 .collect();
308 let valid_match = self.acl.is_valid(
309 &scheme,
310 &authority,
311 lossy_headers.iter().map(|(k, v)| (*k, v.as_ref())),
312 req.body().and_then(|b| b.as_bytes()),
313 );
314 if valid_match.is_denied() {
315 return Err(Error::Middleware(anyhow!(
316 "request is denied - {}",
317 valid_match
318 )));
319 }
320
321 if self.acl.has_modify_request() {
322 let (headers, mut raw_headers) = mutation_headers(req.headers());
323 let mut mutation = RequestMutation {
324 headers,
325 body: req
326 .body()
327 .and_then(|b| b.as_bytes())
328 .map(Bytes::copy_from_slice),
329 };
330 self.acl.modify_request(&scheme, &authority, &mut mutation);
331
332 req.headers_mut().clear();
333 for (name, value) in &mutation.headers {
334 let header_name = HeaderName::from_str(name).map_err(|e| {
335 Error::Middleware(anyhow!("invalid header name `{name}`: {e}"))
336 })?;
337 let header_value = match take_raw_header(&mut raw_headers, name, value) {
338 Some(raw) => raw,
339 None => HeaderValue::from_str(value).map_err(|e| {
340 Error::Middleware(anyhow!("invalid header value for `{name}`: {e}"))
341 })?,
342 };
343 req.headers_mut().append(header_name, header_value);
344 }
345 if let Some(body) = mutation.body {
346 *req.body_mut() = Some(Body::from(body));
347 }
348 }
349
350 let mut res = next.run(req, extensions).await?;
351
352 if self.acl.has_modify_response() {
353 let status = res.status();
354 let version = res.version();
355 let url = res.url().clone();
356 let extensions_snapshot = res.extensions().clone();
357 let (headers, mut raw_headers) = mutation_headers(res.headers());
358 let body = res.bytes().await?;
361
362 let mut mutation = ResponseMutation {
363 status: status.as_u16(),
364 headers,
365 body,
366 };
367 self.acl.modify_response(&scheme, &authority, &mut mutation);
368
369 let mut builder = http::Response::builder()
370 .status(mutation.status)
371 .version(version);
372 for (name, value) in &mutation.headers {
373 builder = match take_raw_header(&mut raw_headers, name, value) {
374 Some(raw) => builder.header(name.as_str(), raw),
375 None => builder.header(name.as_str(), value.as_str()),
376 };
377 }
378 if let Some(ext) = builder.extensions_mut() {
385 *ext = extensions_snapshot;
386 }
387 let http_response = builder
388 .url(url)
389 .body(mutation.body)
390 .map_err(|e| Error::Middleware(anyhow!("failed to rebuild response: {e}")))?;
391 res = Response::from(http_response);
392 }
393
394 Ok(res)
395 } else {
396 return Err(Error::Middleware(anyhow!("missing host")));
397 }
398 }
399}
400
401fn header_value_lossy(value: &HeaderValue) -> Cow<'_, str> {
404 String::from_utf8_lossy(value.as_bytes())
405}
406
407type RawHeader = (String, String, HeaderValue);
410
411fn mutation_headers(headers: &http::HeaderMap) -> (Vec<(String, String)>, Vec<RawHeader>) {
416 let mut raw = Vec::new();
417 let list = headers
418 .iter()
419 .map(|(k, v)| {
420 let name = k.as_str().to_string();
421 let value = header_value_lossy(v).into_owned();
422 if std::str::from_utf8(v.as_bytes()).is_err() {
423 raw.push((name.clone(), value.clone(), v.clone()));
424 }
425 (name, value)
426 })
427 .collect();
428 (list, raw)
429}
430
431fn take_raw_header(raw: &mut Vec<RawHeader>, name: &str, value: &str) -> Option<HeaderValue> {
434 let index = raw
435 .iter()
436 .position(|(n, v, _)| n.eq_ignore_ascii_case(name) && v == value)?;
437 Some(raw.remove(index).2)
438}
439
440type BoxError = Box<dyn std::error::Error + Send + Sync>;
441
442struct GaiResolver;
443
444impl Resolve for GaiResolver {
445 fn resolve(&self, name: Name) -> Resolving {
446 Box::pin(async move {
447 let addresses = (name.as_str(), 0)
450 .to_socket_addrs()
451 .map_err(|e| Box::new(e) as BoxError)?;
452 Ok(Box::new(addresses.into_iter()) as Box<dyn Iterator<Item = SocketAddr> + Send>)
453 })
454 }
455}
456
457pub struct HttpAclDnsResolver {
469 dns_resolver: Arc<dyn Resolve>,
470 acl: Arc<HttpAcl>,
471}
472
473impl HttpAclDnsResolver {
474 pub fn new(middleware: &HttpAclMiddleware) -> Self {
476 Self {
477 dns_resolver: Arc::new(GaiResolver),
478 acl: middleware.acl(),
479 }
480 }
481
482 pub fn with_dns_resolver(
485 middleware: &HttpAclMiddleware,
486 dns_resolver: Arc<dyn Resolve>,
487 ) -> Self {
488 Self {
489 dns_resolver,
490 acl: middleware.acl(),
491 }
492 }
493}
494
495impl Resolve for HttpAclDnsResolver {
496 fn resolve(&self, name: Name) -> Resolving {
497 if self.acl.is_host_allowed(name.as_str()).is_denied() {
498 let err: BoxError = Box::new(HttpAclError::HostDenied {
499 host: name.as_str().to_string(),
500 });
501 return Box::pin(future::ready(Err(err)));
502 }
503
504 let acl = self.acl.clone();
505 let resolver = self.dns_resolver.clone();
506
507 Box::pin(async move {
508 if let Some(tcp_address) = acl.resolve_trusted_static_dns_mapping(name.as_str()) {
509 Ok(Box::new(std::iter::once(tcp_address))
513 as Box<dyn Iterator<Item = SocketAddr> + Send>)
514 } else if let Some(tcp_address) = acl.resolve_static_dns_mapping(name.as_str()) {
515 if acl.is_ip_allowed(&tcp_address.ip()).is_allowed()
520 && acl.is_port_allowed(tcp_address.port()).is_allowed()
521 {
522 Ok(Box::new(std::iter::once(tcp_address))
523 as Box<dyn Iterator<Item = SocketAddr> + Send>)
524 } else {
525 let err: BoxError =
526 Box::new(std::io::Error::other("Static DNS mapping denied by ACL"));
527 Err(err)
528 }
529 } else {
530 let resolved = resolver.resolve(name).await;
531 match resolved {
532 Ok(addresses) => {
533 let filtered = addresses
534 .into_iter()
535 .filter(|addr| {
536 acl.is_ip_allowed(&addr.ip()).is_allowed()
537 && acl.is_port_allowed(addr.port()).is_allowed()
538 })
539 .collect::<Vec<_>>();
540 Ok(Box::new(filtered.into_iter())
541 as Box<dyn Iterator<Item = SocketAddr> + Send>)
542 }
543 Err(e) => Err(e),
544 }
545 }
546 })
547 }
548}
549
550#[derive(Error, Debug)]
551pub enum HttpAclError {
557 #[error("Host resolution denied by ACL: {host}")]
559 HostDenied {
560 host: String,
562 },
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use http_acl::acl::AclClassification;
569 use reqwest_middleware::ClientWithMiddleware;
570
571 #[tokio::test]
572 async fn test_http_acl_middleware() {
573 let acl = HttpAcl::builder()
574 .add_denied_host("example.com".to_string())
575 .unwrap()
576 .build();
577
578 let middleware = HttpAclMiddleware::new(acl);
579
580 let client = reqwest_middleware::ClientBuilder::new(
581 reqwest::Client::builder()
582 .dns_resolver(middleware.dns_resolver())
583 .build()
584 .unwrap(),
585 )
586 .with(middleware)
587 .build();
588
589 let request = client.get("http://example.com/").send().await;
590
591 assert!(request.is_err());
592 assert_eq!(
593 request.unwrap_err().to_string(),
594 "host example.com is denied - The entity is denied according to the denied ACL."
595 );
596 }
597
598 #[tokio::test]
599 async fn test_middleware_decodes_percent_encoded_path() {
600 let acl = HttpAcl::builder()
601 .add_allowed_host("example.com".to_string())
602 .unwrap()
603 .add_denied_url_path("/secret file".to_string())
604 .unwrap()
605 .build();
606
607 let middleware = HttpAclMiddleware::new(acl);
608
609 let client =
610 reqwest_middleware::ClientBuilder::new(reqwest::Client::builder().build().unwrap())
611 .with(middleware)
612 .build();
613
614 let request = client.get("http://example.com/secret%20file").send().await;
619
620 assert!(request.is_err());
621 assert!(
622 request
623 .unwrap_err()
624 .to_string()
625 .contains("path /secret file is denied")
626 );
627 }
628
629 #[tokio::test]
630 async fn test_dns_resolver_returns_typed_error_for_denied_host() {
631 let acl = HttpAcl::builder()
632 .add_denied_host("denied.example.com".to_string())
633 .unwrap()
634 .build();
635
636 let middleware = HttpAclMiddleware::new(acl);
637 let resolver = middleware.dns_resolver();
638
639 let name: reqwest::dns::Name = "denied.example.com".parse().unwrap();
640 let err = match resolver.resolve(name).await {
641 Ok(_) => panic!("expected resolution to be denied"),
642 Err(e) => e,
643 };
644
645 let acl_err = err
646 .downcast_ref::<HttpAclError>()
647 .expect("expected a HttpAclError");
648 assert!(matches!(
649 acl_err,
650 HttpAclError::HostDenied { host } if host == "denied.example.com"
651 ));
652 }
653
654 #[tokio::test]
655 async fn test_dns_resolver_resolves_hostnames() {
656 use tokio::io::{AsyncReadExt, AsyncWriteExt};
657 use tokio::net::TcpListener;
658
659 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
660 let addr = listener.local_addr().unwrap();
661
662 tokio::spawn(async move {
663 if let Ok((mut socket, _)) = listener.accept().await {
664 let mut buf = [0u8; 1024];
665 let _ = socket.read(&mut buf).await;
666 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
667 let _ = socket.write_all(response.as_bytes()).await;
668 }
669 });
670
671 let acl = HttpAcl::builder()
672 .non_global_ip_ranges(true)
673 .ip_acl_default(true)
674 .port_acl_default(true)
675 .host_acl_default(true)
676 .build();
677
678 let middleware = HttpAclMiddleware::new(acl);
679
680 let client = reqwest_middleware::ClientBuilder::new(
681 reqwest::Client::builder()
682 .dns_resolver(middleware.dns_resolver())
683 .build()
684 .unwrap(),
685 )
686 .with(middleware)
687 .build();
688
689 let request = client
692 .get(format!("http://localhost:{}/", addr.port()))
693 .send()
694 .await;
695
696 assert!(request.is_ok(), "{:?}", request.err());
697 }
698
699 #[tokio::test]
700 async fn test_trusted_static_dns_mapping_bypasses_ip_port_acl() {
701 use tokio::io::{AsyncReadExt, AsyncWriteExt};
702 use tokio::net::TcpListener;
703
704 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
705 let addr = listener.local_addr().unwrap();
706
707 tokio::spawn(async move {
708 if let Ok((mut socket, _)) = listener.accept().await {
709 let mut buf = [0u8; 1024];
710 let _ = socket.read(&mut buf).await;
711 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
712 let _ = socket.write_all(response.as_bytes()).await;
713 }
714 });
715
716 let acl = HttpAcl::builder()
719 .host_acl_default(true)
720 .add_trusted_static_dns_mapping("trusted.internal".to_string(), addr)
721 .unwrap()
722 .build();
723
724 assert!(acl.is_ip_allowed(&addr.ip()).is_denied());
725 assert!(acl.is_port_allowed(addr.port()).is_denied());
726
727 let middleware = HttpAclMiddleware::new(acl);
728
729 let client = reqwest_middleware::ClientBuilder::new(
730 reqwest::Client::builder()
731 .dns_resolver(middleware.dns_resolver())
732 .build()
733 .unwrap(),
734 )
735 .with(middleware)
736 .build();
737
738 let request = client.get("http://trusted.internal/").send().await;
741
742 assert!(request.is_ok(), "{:?}", request.err());
743 }
744
745 #[tokio::test]
746 async fn test_redirect_policy_blocks_disallowed_target() {
747 use tokio::io::{AsyncReadExt, AsyncWriteExt};
748 use tokio::net::TcpListener;
749
750 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
751 let addr = listener.local_addr().unwrap();
752
753 tokio::spawn(async move {
754 if let Ok((mut socket, _)) = listener.accept().await {
755 let mut buf = [0u8; 1024];
756 let _ = socket.read(&mut buf).await;
757 let response = "HTTP/1.1 302 Found\r\nLocation: http://192.168.1.1/\r\nContent-Length: 0\r\n\r\n";
758 let _ = socket.write_all(response.as_bytes()).await;
759 }
760 });
761
762 let acl = HttpAcl::builder()
765 .non_global_ip_ranges(true)
766 .ip_acl_default(true)
767 .port_acl_default(true)
768 .add_denied_ip_range((
769 "192.168.1.1".parse::<std::net::IpAddr>().unwrap(),
770 "192.168.1.1".parse::<std::net::IpAddr>().unwrap(),
771 ))
772 .unwrap()
773 .build();
774
775 let middleware = HttpAclMiddleware::new(acl);
776
777 let client = reqwest_middleware::ClientBuilder::new(
778 reqwest::Client::builder()
779 .dns_resolver(middleware.dns_resolver())
780 .redirect(middleware.redirect_policy())
781 .build()
782 .unwrap(),
783 )
784 .with(middleware)
785 .build();
786
787 let request = client
788 .get(format!("http://127.0.0.1:{}/", addr.port()))
789 .send()
790 .await;
791
792 assert!(request.is_err());
793 }
794
795 #[test]
796 fn test_no_hooks_configured_leaves_acl_unaffected() {
797 let acl = HttpAcl::builder().build();
798
799 assert!(!acl.has_modify_request());
800 assert!(!acl.has_modify_response());
801 }
802
803 #[tokio::test]
804 async fn test_modify_request_injects_header() {
805 use tokio::io::{AsyncReadExt, AsyncWriteExt};
806 use tokio::net::TcpListener;
807
808 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
809 let addr = listener.local_addr().unwrap();
810
811 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
812 tokio::spawn(async move {
813 if let Ok((mut socket, _)) = listener.accept().await {
814 let mut buf = [0u8; 4096];
815 let n = socket.read(&mut buf).await.unwrap_or(0);
816 let _ = captured_tx.send(String::from_utf8_lossy(&buf[..n]).into_owned());
817 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
818 let _ = socket.write_all(response.as_bytes()).await;
819 }
820 });
821
822 let acl = HttpAcl::builder()
823 .non_global_ip_ranges(true)
824 .ip_acl_default(true)
825 .port_acl_default(true)
826 .host_acl_default(true)
827 .build_full(HttpAclHooks {
828 modify_request_fn: Some(Arc::new(|_scheme, _authority, mutation| {
829 mutation
830 .headers
831 .push(("x-injected-secret".to_string(), "sssh".to_string()));
832 })),
833 ..Default::default()
834 });
835
836 let middleware = HttpAclMiddleware::new(acl);
837
838 let client = reqwest_middleware::ClientBuilder::new(
839 reqwest::Client::builder()
840 .dns_resolver(middleware.dns_resolver())
841 .build()
842 .unwrap(),
843 )
844 .with(middleware)
845 .build();
846
847 let request = client
848 .get(format!("http://127.0.0.1:{}/", addr.port()))
849 .send()
850 .await;
851
852 assert!(request.is_ok(), "{:?}", request.err());
853 let captured = captured_rx.await.unwrap();
854 assert!(
855 captured.to_lowercase().contains("x-injected-secret: sssh"),
856 "captured request did not contain the injected header:\n{captured}"
857 );
858 }
859
860 #[tokio::test]
861 async fn test_modify_request_replaces_body_and_content_length() {
862 use tokio::io::{AsyncReadExt, AsyncWriteExt};
863 use tokio::net::TcpListener;
864
865 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
866 let addr = listener.local_addr().unwrap();
867
868 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
869 tokio::spawn(async move {
870 if let Ok((mut socket, _)) = listener.accept().await {
871 let mut buf = [0u8; 4096];
872 let n = socket.read(&mut buf).await.unwrap_or(0);
873 let _ = captured_tx.send(String::from_utf8_lossy(&buf[..n]).into_owned());
874 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
875 let _ = socket.write_all(response.as_bytes()).await;
876 }
877 });
878
879 let new_body = "a much longer replacement body than the original";
880 let acl = HttpAcl::builder()
881 .non_global_ip_ranges(true)
882 .ip_acl_default(true)
883 .port_acl_default(true)
884 .host_acl_default(true)
885 .build_full(HttpAclHooks {
886 modify_request_fn: Some(Arc::new(move |_scheme, _authority, mutation| {
887 mutation.body = Some(Bytes::from(new_body));
888 })),
889 ..Default::default()
890 });
891
892 let middleware = HttpAclMiddleware::new(acl);
893
894 let client = reqwest_middleware::ClientBuilder::new(
895 reqwest::Client::builder()
896 .dns_resolver(middleware.dns_resolver())
897 .build()
898 .unwrap(),
899 )
900 .with(middleware)
901 .build();
902
903 let request = client
904 .post(format!("http://127.0.0.1:{}/", addr.port()))
905 .body("short")
906 .send()
907 .await;
908
909 assert!(request.is_ok(), "{:?}", request.err());
910 let captured = captured_rx.await.unwrap();
911 assert!(
912 captured.contains(&format!("content-length: {}", new_body.len()))
913 || captured.contains(&format!("Content-Length: {}", new_body.len())),
914 "captured request did not have a Content-Length matching the replaced body:\n{captured}"
915 );
916 assert!(
917 captured.ends_with(new_body),
918 "captured request did not end with the replaced body:\n{captured}"
919 );
920 }
921
922 #[tokio::test]
923 async fn test_modify_response_rewrites_status_headers_and_body() {
924 use tokio::io::{AsyncReadExt, AsyncWriteExt};
925 use tokio::net::TcpListener;
926
927 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
928 let addr = listener.local_addr().unwrap();
929
930 tokio::spawn(async move {
931 if let Ok((mut socket, _)) = listener.accept().await {
932 let mut buf = [0u8; 1024];
933 let _ = socket.read(&mut buf).await;
934 let body = "original body";
935 let response = format!(
936 "HTTP/1.1 200 OK\r\nx-remove-me: yes\r\nContent-Length: {}\r\n\r\n{}",
937 body.len(),
938 body
939 );
940 let _ = socket.write_all(response.as_bytes()).await;
941 }
942 });
943
944 let acl = HttpAcl::builder()
945 .non_global_ip_ranges(true)
946 .ip_acl_default(true)
947 .port_acl_default(true)
948 .host_acl_default(true)
949 .build_full(HttpAclHooks {
950 modify_response_fn: Some(Arc::new(|_scheme, _authority, mutation| {
951 mutation.status = 201;
952 mutation.headers.retain(|(k, _)| k != "x-remove-me");
953 mutation
954 .headers
955 .push(("x-added".to_string(), "yes".to_string()));
956 mutation.body = Bytes::from_static(b"redacted body");
957 })),
958 ..Default::default()
959 });
960
961 let middleware = HttpAclMiddleware::new(acl);
962
963 let client = reqwest_middleware::ClientBuilder::new(
964 reqwest::Client::builder()
965 .dns_resolver(middleware.dns_resolver())
966 .build()
967 .unwrap(),
968 )
969 .with(middleware)
970 .build();
971
972 let response = client
973 .get(format!("http://127.0.0.1:{}/", addr.port()))
974 .send()
975 .await
976 .unwrap();
977
978 assert_eq!(response.status(), 201);
979 assert!(!response.headers().contains_key("x-remove-me"));
980 assert_eq!(response.headers().get("x-added").unwrap(), "yes");
981 let body = response.text().await.unwrap();
982 assert_eq!(body, "redacted body");
983 }
984
985 #[tokio::test]
986 async fn test_modify_response_preserves_url_and_remote_addr() {
987 use tokio::io::{AsyncReadExt, AsyncWriteExt};
988 use tokio::net::TcpListener;
989
990 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
991 let addr = listener.local_addr().unwrap();
992
993 tokio::spawn(async move {
994 if let Ok((mut socket, _)) = listener.accept().await {
995 let mut buf = [0u8; 1024];
996 let _ = socket.read(&mut buf).await;
997 let response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
998 let _ = socket.write_all(response.as_bytes()).await;
999 }
1000 });
1001
1002 let acl = HttpAcl::builder()
1003 .non_global_ip_ranges(true)
1004 .ip_acl_default(true)
1005 .port_acl_default(true)
1006 .host_acl_default(true)
1007 .build_full(HttpAclHooks {
1008 modify_response_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1009 mutation.body = Bytes::from_static(b"changed");
1010 })),
1011 ..Default::default()
1012 });
1013
1014 let middleware = HttpAclMiddleware::new(acl);
1015
1016 let client = reqwest_middleware::ClientBuilder::new(
1017 reqwest::Client::builder()
1018 .dns_resolver(middleware.dns_resolver())
1019 .build()
1020 .unwrap(),
1021 )
1022 .with(middleware)
1023 .build();
1024
1025 let url = format!("http://127.0.0.1:{}/", addr.port());
1026 let response = client.get(&url).send().await.unwrap();
1027
1028 assert_eq!(response.url().as_str(), url);
1029 assert!(
1030 response.remote_addr().is_some(),
1031 "remote_addr() was lost when the response was rebuilt"
1032 );
1033 }
1034
1035 #[tokio::test]
1036 async fn test_modify_request_header_carries_through_redirect() {
1037 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1038 use tokio::net::TcpListener;
1039
1040 let second_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1041 let second_addr = second_listener.local_addr().unwrap();
1042
1043 let (second_captured_tx, second_captured_rx) = tokio::sync::oneshot::channel();
1044 tokio::spawn(async move {
1045 if let Ok((mut socket, _)) = second_listener.accept().await {
1046 let mut buf = [0u8; 4096];
1047 let n = socket.read(&mut buf).await.unwrap_or(0);
1048 let _ = second_captured_tx.send(String::from_utf8_lossy(&buf[..n]).into_owned());
1049 let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
1050 let _ = socket.write_all(response.as_bytes()).await;
1051 }
1052 });
1053
1054 let first_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1055 let first_addr = first_listener.local_addr().unwrap();
1056 tokio::spawn(async move {
1057 if let Ok((mut socket, _)) = first_listener.accept().await {
1058 let mut buf = [0u8; 1024];
1059 let _ = socket.read(&mut buf).await;
1060 let response = format!(
1061 "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{}/\r\nContent-Length: 0\r\n\r\n",
1062 second_addr.port()
1063 );
1064 let _ = socket.write_all(response.as_bytes()).await;
1065 }
1066 });
1067
1068 let acl = HttpAcl::builder()
1069 .non_global_ip_ranges(true)
1070 .ip_acl_default(true)
1071 .port_acl_default(true)
1072 .host_acl_default(true)
1073 .build_full(HttpAclHooks {
1074 modify_request_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1075 mutation
1076 .headers
1077 .push(("x-marker".to_string(), "hop-one-only".to_string()));
1078 })),
1079 ..Default::default()
1080 });
1081
1082 let middleware = HttpAclMiddleware::new(acl);
1083
1084 let client = reqwest_middleware::ClientBuilder::new(
1085 reqwest::Client::builder()
1086 .dns_resolver(middleware.dns_resolver())
1087 .redirect(middleware.redirect_policy())
1088 .build()
1089 .unwrap(),
1090 )
1091 .with(middleware)
1092 .build();
1093
1094 let request = client
1095 .get(format!("http://127.0.0.1:{}/", first_addr.port()))
1096 .send()
1097 .await;
1098
1099 assert!(request.is_ok(), "{:?}", request.err());
1100 let second_captured = second_captured_rx.await.unwrap();
1101 assert!(
1107 second_captured.to_lowercase().contains("x-marker"),
1108 "expected the header injected for the original request to carry through \
1109 reqwest's redirect handling to the second hop, but it did not:\n{second_captured}"
1110 );
1111 }
1112
1113 async fn capture_server(
1116 response: &'static [u8],
1117 ) -> (SocketAddr, tokio::sync::oneshot::Receiver<Vec<u8>>) {
1118 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1119 use tokio::net::TcpListener;
1120
1121 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1122 let addr = listener.local_addr().unwrap();
1123 let (captured_tx, captured_rx) = tokio::sync::oneshot::channel();
1124 tokio::spawn(async move {
1125 if let Ok((mut socket, _)) = listener.accept().await {
1126 let mut buf = [0u8; 4096];
1127 let n = socket.read(&mut buf).await.unwrap_or(0);
1128 let _ = captured_tx.send(buf[..n].to_ascii_lowercase());
1129 let _ = socket.write_all(response).await;
1130 }
1131 });
1132 (addr, captured_rx)
1133 }
1134
1135 fn local_client(hooks: HttpAclHooks, builder: HttpAclBuilder) -> ClientWithMiddleware {
1136 let acl = builder
1137 .non_global_ip_ranges(true)
1138 .ip_acl_default(true)
1139 .port_acl_default(true)
1140 .host_acl_default(true)
1141 .build_full(hooks);
1142 let middleware = HttpAclMiddleware::new(acl);
1143 reqwest_middleware::ClientBuilder::new(
1144 reqwest::Client::builder()
1145 .dns_resolver(middleware.dns_resolver())
1146 .build()
1147 .unwrap(),
1148 )
1149 .with(middleware)
1150 .build()
1151 }
1152
1153 fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1154 haystack.windows(needle.len()).any(|w| w == needle)
1155 }
1156
1157 const OK_EMPTY: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
1158
1159 #[tokio::test]
1160 async fn test_obs_text_header_values_are_sent_unchanged() {
1161 let (addr, captured_rx) = capture_server(OK_EMPTY).await;
1162 let client = local_client(HttpAclHooks::default(), HttpAcl::builder());
1163
1164 let request = client
1165 .get(format!("http://127.0.0.1:{}/", addr.port()))
1166 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1167 .header("user-agent", HeaderValue::from_bytes(b"caf\xE9").unwrap())
1168 .send()
1169 .await;
1170
1171 assert!(request.is_ok(), "{:?}", request.err());
1172 let captured = captured_rx.await.unwrap();
1173 assert!(
1174 contains(&captured, b"cookie: a=caf\xE9\r\n"),
1175 "{captured:?}"
1176 );
1177 assert!(
1178 contains(&captured, b"user-agent: caf\xE9\r\n"),
1179 "{captured:?}"
1180 );
1181 }
1182
1183 #[tokio::test]
1184 async fn test_denied_header_with_obs_text_value_is_still_denied() {
1185 let client = local_client(
1186 HttpAclHooks::default(),
1187 HttpAcl::builder()
1188 .add_denied_header("cookie".to_string(), None)
1189 .unwrap(),
1190 );
1191
1192 let request = client
1193 .get("http://127.0.0.1:9/")
1194 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1195 .send()
1196 .await;
1197
1198 let error = request.unwrap_err().to_string();
1199 assert!(error.starts_with("header cookie: "), "{error}");
1200 assert!(error.contains("is denied"), "{error}");
1201 }
1202
1203 #[tokio::test]
1204 async fn test_allowed_header_value_is_compared_as_raw_bytes() {
1205 let builder = || {
1208 HttpAcl::builder()
1209 .add_allowed_header("x-token".to_string(), Some("caf\u{FFFD}".to_string()))
1210 .unwrap()
1211 };
1212
1213 let client = local_client(HttpAclHooks::default(), builder());
1214 let request = client
1215 .get("http://127.0.0.1:9/")
1216 .header("x-token", HeaderValue::from_bytes(b"caf\xE9").unwrap())
1217 .send()
1218 .await;
1219 let error = request.unwrap_err().to_string();
1220 assert!(error.starts_with("header x-token: "), "{error}");
1221
1222 let (addr, captured_rx) = capture_server(OK_EMPTY).await;
1223 let client = local_client(HttpAclHooks::default(), builder());
1224 let request = client
1225 .get(format!("http://127.0.0.1:{}/", addr.port()))
1226 .header(
1227 "x-token",
1228 HeaderValue::from_bytes("caf\u{FFFD}".as_bytes()).unwrap(),
1229 )
1230 .send()
1231 .await;
1232 assert!(request.is_ok(), "{:?}", request.err());
1233 let captured = captured_rx.await.unwrap();
1234 assert!(
1235 contains(&captured, "x-token: caf\u{FFFD}\r\n".as_bytes()),
1236 "{captured:?}"
1237 );
1238 }
1239
1240 #[tokio::test]
1241 async fn test_validate_fn_sees_obs_text_headers() {
1242 let client = local_client(
1245 HttpAclHooks {
1246 validate_fn: Some(Arc::new(|_scheme, _authority, mut headers, _body| {
1247 if headers.any(|(k, v)| k == "cookie" && v == "a=caf\u{FFFD}") {
1248 AclClassification::Denied("cookie seen".to_string())
1249 } else {
1250 AclClassification::AllowedDefault
1251 }
1252 })),
1253 ..Default::default()
1254 },
1255 HttpAcl::builder(),
1256 );
1257
1258 let request = client
1259 .get("http://127.0.0.1:9/")
1260 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1261 .send()
1262 .await;
1263
1264 let error = request.unwrap_err().to_string();
1265 assert!(error.contains("cookie seen"), "{error}");
1266 }
1267
1268 #[tokio::test]
1269 async fn test_modify_request_keeps_obs_text_header_bytes() {
1270 let (addr, captured_rx) = capture_server(OK_EMPTY).await;
1271 let client = local_client(
1272 HttpAclHooks {
1273 modify_request_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1274 mutation.headers.retain(|(k, _)| k != "x-drop-me");
1275 mutation
1276 .headers
1277 .push(("x-injected".to_string(), "yes".to_string()));
1278 })),
1279 ..Default::default()
1280 },
1281 HttpAcl::builder(),
1282 );
1283
1284 let request = client
1285 .get(format!("http://127.0.0.1:{}/", addr.port()))
1286 .header("cookie", HeaderValue::from_bytes(b"a=caf\xE9").unwrap())
1287 .header("x-drop-me", HeaderValue::from_bytes(b"caf\xE9").unwrap())
1288 .send()
1289 .await;
1290
1291 assert!(request.is_ok(), "{:?}", request.err());
1292 let captured = captured_rx.await.unwrap();
1293 assert!(
1294 contains(&captured, b"cookie: a=caf\xE9\r\n"),
1295 "{captured:?}"
1296 );
1297 assert!(contains(&captured, b"x-injected: yes\r\n"), "{captured:?}");
1298 assert!(!contains(&captured, b"x-drop-me"), "{captured:?}");
1299 }
1300
1301 #[tokio::test]
1302 async fn test_modify_response_keeps_obs_text_header_bytes() {
1303 let (addr, _captured_rx) =
1304 capture_server(b"HTTP/1.1 200 OK\r\nx-raw: caf\xE9\r\nContent-Length: 2\r\n\r\nok")
1305 .await;
1306 let client = local_client(
1307 HttpAclHooks {
1308 modify_response_fn: Some(Arc::new(|_scheme, _authority, mutation| {
1309 mutation.body = Bytes::from_static(b"changed");
1310 })),
1311 ..Default::default()
1312 },
1313 HttpAcl::builder(),
1314 );
1315
1316 let response = client
1317 .get(format!("http://127.0.0.1:{}/", addr.port()))
1318 .send()
1319 .await
1320 .unwrap();
1321
1322 assert_eq!(
1323 response.headers().get("x-raw").unwrap().as_bytes(),
1324 b"caf\xE9"
1325 );
1326 assert_eq!(response.text().await.unwrap(), "changed");
1327 }
1328}